@otto-assistant/bridge 0.4.92
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-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -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/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -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 +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -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/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -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 +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -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 +302 -0
- package/dist/format-tables.test.js +308 -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 +483 -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 +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -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 +263 -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 +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -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 +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -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 +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -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 +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -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 +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -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 +114 -0
- package/dist/utils.js +144 -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 +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -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/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -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/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -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-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -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/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -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 +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -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/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -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 +335 -0
- package/src/format-tables.ts +445 -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 +640 -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 +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -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 +314 -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 +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -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 +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.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 +68 -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 +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -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 +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -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 +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -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 +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -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 +127 -0
- package/src/utils.ts +212 -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 +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// E2e tests for ThreadSessionRuntime lifecycle behaviors.
|
|
2
|
+
// Tests scenarios not covered by the queue/interrupt tests:
|
|
3
|
+
// 1. Sequential completions: listener stays alive across multiple full run cycles
|
|
4
|
+
// 2. Concurrent first messages: runtime serialization without threadMessageQueue
|
|
5
|
+
//
|
|
6
|
+
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
7
|
+
// Poll timeouts: 4s max, 100ms interval.
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import url from 'node:url'
|
|
13
|
+
import { describe, beforeAll, afterAll, test, expect } from 'vitest'
|
|
14
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
|
|
15
|
+
import { DigitalDiscord } from 'discord-digital-twin/src'
|
|
16
|
+
import {
|
|
17
|
+
buildDeterministicOpencodeConfig,
|
|
18
|
+
type DeterministicMatcher,
|
|
19
|
+
} from 'opencode-deterministic-provider'
|
|
20
|
+
import { setDataDir } from './config.js'
|
|
21
|
+
import { store } from './store.js'
|
|
22
|
+
import { startDiscordBot } from './discord-bot.js'
|
|
23
|
+
import { getRuntime } from './session-handler/thread-session-runtime.js'
|
|
24
|
+
import {
|
|
25
|
+
setBotToken,
|
|
26
|
+
initDatabase,
|
|
27
|
+
closeDatabase,
|
|
28
|
+
setChannelDirectory,
|
|
29
|
+
setChannelVerbosity,
|
|
30
|
+
type VerbosityLevel,
|
|
31
|
+
} from './database.js'
|
|
32
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js'
|
|
33
|
+
import {
|
|
34
|
+
initializeOpencodeForDirectory,
|
|
35
|
+
restartOpencodeServer,
|
|
36
|
+
stopOpencodeServer,
|
|
37
|
+
} from './opencode.js'
|
|
38
|
+
import {
|
|
39
|
+
chooseLockPort,
|
|
40
|
+
cleanupTestSessions,
|
|
41
|
+
initTestGitRepo,
|
|
42
|
+
waitForBotMessageContaining,
|
|
43
|
+
waitForBotReplyAfterUserMessage,
|
|
44
|
+
} from './test-utils.js'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const TEST_USER_ID = '200000000000000888'
|
|
48
|
+
const TEXT_CHANNEL_ID = '200000000000000889'
|
|
49
|
+
|
|
50
|
+
function createRunDirectories() {
|
|
51
|
+
const root = path.resolve(process.cwd(), 'tmp', 'runtime-lifecycle-e2e')
|
|
52
|
+
fs.mkdirSync(root, { recursive: true })
|
|
53
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
|
|
54
|
+
const projectDirectory = path.join(root, 'project')
|
|
55
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
56
|
+
initTestGitRepo(projectDirectory)
|
|
57
|
+
return { root, dataDir, projectDirectory }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function createDiscordJsClient({ restUrl }: { restUrl: string }) {
|
|
63
|
+
return new Client({
|
|
64
|
+
intents: [
|
|
65
|
+
GatewayIntentBits.Guilds,
|
|
66
|
+
GatewayIntentBits.GuildMessages,
|
|
67
|
+
GatewayIntentBits.MessageContent,
|
|
68
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
69
|
+
],
|
|
70
|
+
partials: [
|
|
71
|
+
Partials.Channel,
|
|
72
|
+
Partials.Message,
|
|
73
|
+
Partials.User,
|
|
74
|
+
Partials.ThreadMember,
|
|
75
|
+
],
|
|
76
|
+
rest: {
|
|
77
|
+
api: restUrl,
|
|
78
|
+
version: '10',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createDeterministicMatchers(): DeterministicMatcher[] {
|
|
84
|
+
const highUsageReplyMatcher: DeterministicMatcher = {
|
|
85
|
+
id: 'high-usage-reply',
|
|
86
|
+
priority: 20,
|
|
87
|
+
when: {
|
|
88
|
+
lastMessageRole: 'user',
|
|
89
|
+
rawPromptIncludes: 'Reply with exactly: footer-high-usage',
|
|
90
|
+
},
|
|
91
|
+
then: {
|
|
92
|
+
parts: [
|
|
93
|
+
{ type: 'stream-start', warnings: [] },
|
|
94
|
+
{ type: 'text-start', id: 'high-usage-reply' },
|
|
95
|
+
{ type: 'text-delta', id: 'high-usage-reply', delta: 'ok' },
|
|
96
|
+
{ type: 'text-end', id: 'high-usage-reply' },
|
|
97
|
+
{
|
|
98
|
+
type: 'finish',
|
|
99
|
+
finishReason: 'stop',
|
|
100
|
+
usage: { inputTokens: 15_000, outputTokens: 1, totalTokens: 15_001 },
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
partDelaysMs: [0, 100, 0, 0, 0],
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Simple reply matcher: model echoes back the requested text.
|
|
108
|
+
// Uses 100ms delay on first text delta to keep streams async without adding
|
|
109
|
+
// unnecessary latency. Tests verify ordering/serialization, not latency handling.
|
|
110
|
+
const userReplyMatcher: DeterministicMatcher = {
|
|
111
|
+
id: 'user-reply',
|
|
112
|
+
priority: 10,
|
|
113
|
+
when: {
|
|
114
|
+
lastMessageRole: 'user',
|
|
115
|
+
rawPromptIncludes: 'Reply with exactly:',
|
|
116
|
+
},
|
|
117
|
+
then: {
|
|
118
|
+
parts: [
|
|
119
|
+
{ type: 'stream-start', warnings: [] },
|
|
120
|
+
{ type: 'text-start', id: 'default-reply' },
|
|
121
|
+
{ type: 'text-delta', id: 'default-reply', delta: 'ok' },
|
|
122
|
+
{ type: 'text-end', id: 'default-reply' },
|
|
123
|
+
{
|
|
124
|
+
type: 'finish',
|
|
125
|
+
finishReason: 'stop',
|
|
126
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
partDelaysMs: [0, 100, 0, 0, 0],
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [highUsageReplyMatcher, userReplyMatcher]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('runtime lifecycle', () => {
|
|
137
|
+
let directories: ReturnType<typeof createRunDirectories>
|
|
138
|
+
let discord: DigitalDiscord
|
|
139
|
+
let botClient: Client
|
|
140
|
+
let previousDefaultVerbosity: VerbosityLevel | null = null
|
|
141
|
+
let testStartTime = Date.now()
|
|
142
|
+
|
|
143
|
+
beforeAll(async () => {
|
|
144
|
+
testStartTime = Date.now()
|
|
145
|
+
directories = createRunDirectories()
|
|
146
|
+
const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
|
|
147
|
+
|
|
148
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
|
|
149
|
+
setDataDir(directories.dataDir)
|
|
150
|
+
previousDefaultVerbosity = store.getState().defaultVerbosity
|
|
151
|
+
store.setState({ defaultVerbosity: 'tools_and_text' })
|
|
152
|
+
|
|
153
|
+
const digitalDiscordDbPath = path.join(
|
|
154
|
+
directories.dataDir,
|
|
155
|
+
'digital-discord.db',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
discord = new DigitalDiscord({
|
|
159
|
+
guild: {
|
|
160
|
+
name: 'Lifecycle E2E Guild',
|
|
161
|
+
ownerId: TEST_USER_ID,
|
|
162
|
+
},
|
|
163
|
+
channels: [
|
|
164
|
+
{
|
|
165
|
+
id: TEXT_CHANNEL_ID,
|
|
166
|
+
name: 'lifecycle-e2e',
|
|
167
|
+
type: ChannelType.GuildText,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
users: [
|
|
171
|
+
{
|
|
172
|
+
id: TEST_USER_ID,
|
|
173
|
+
username: 'lifecycle-tester',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await discord.start()
|
|
180
|
+
|
|
181
|
+
const providerNpm = url
|
|
182
|
+
.pathToFileURL(
|
|
183
|
+
path.resolve(
|
|
184
|
+
process.cwd(),
|
|
185
|
+
'..',
|
|
186
|
+
'opencode-deterministic-provider',
|
|
187
|
+
'src',
|
|
188
|
+
'index.ts',
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
.toString()
|
|
192
|
+
|
|
193
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
194
|
+
providerName: 'deterministic-provider',
|
|
195
|
+
providerNpm,
|
|
196
|
+
model: 'deterministic-v2',
|
|
197
|
+
smallModel: 'deterministic-v2',
|
|
198
|
+
settings: {
|
|
199
|
+
strict: false,
|
|
200
|
+
matchers: createDeterministicMatchers(),
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(directories.projectDirectory, 'opencode.json'),
|
|
205
|
+
JSON.stringify(opencodeConfig, null, 2),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
|
|
209
|
+
const hranaResult = await startHranaServer({ dbPath })
|
|
210
|
+
if (hranaResult instanceof Error) {
|
|
211
|
+
throw hranaResult
|
|
212
|
+
}
|
|
213
|
+
process.env['KIMAKI_DB_URL'] = hranaResult
|
|
214
|
+
await initDatabase()
|
|
215
|
+
await setBotToken(discord.botUserId, discord.botToken)
|
|
216
|
+
|
|
217
|
+
await setChannelDirectory({
|
|
218
|
+
channelId: TEXT_CHANNEL_ID,
|
|
219
|
+
directory: directories.projectDirectory,
|
|
220
|
+
channelType: 'text',
|
|
221
|
+
})
|
|
222
|
+
await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
|
|
223
|
+
|
|
224
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl })
|
|
225
|
+
await startDiscordBot({
|
|
226
|
+
token: discord.botToken,
|
|
227
|
+
appId: discord.botUserId,
|
|
228
|
+
discordClient: botClient,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Pre-warm the opencode server
|
|
232
|
+
const warmup = await initializeOpencodeForDirectory(
|
|
233
|
+
directories.projectDirectory,
|
|
234
|
+
)
|
|
235
|
+
if (warmup instanceof Error) {
|
|
236
|
+
throw warmup
|
|
237
|
+
}
|
|
238
|
+
}, 60_000)
|
|
239
|
+
|
|
240
|
+
afterAll(async () => {
|
|
241
|
+
if (directories) {
|
|
242
|
+
await cleanupTestSessions({
|
|
243
|
+
projectDirectory: directories.projectDirectory,
|
|
244
|
+
testStartTime,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
if (botClient) {
|
|
248
|
+
botClient.destroy()
|
|
249
|
+
}
|
|
250
|
+
await stopOpencodeServer()
|
|
251
|
+
await Promise.all([
|
|
252
|
+
closeDatabase().catch(() => { return }),
|
|
253
|
+
stopHranaServer().catch(() => { return }),
|
|
254
|
+
discord?.stop().catch(() => { return }),
|
|
255
|
+
])
|
|
256
|
+
delete process.env['KIMAKI_LOCK_PORT']
|
|
257
|
+
delete process.env['KIMAKI_DB_URL']
|
|
258
|
+
if (previousDefaultVerbosity) {
|
|
259
|
+
store.setState({ defaultVerbosity: previousDefaultVerbosity })
|
|
260
|
+
}
|
|
261
|
+
if (directories) {
|
|
262
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true })
|
|
263
|
+
}
|
|
264
|
+
}, 10_000)
|
|
265
|
+
|
|
266
|
+
test(
|
|
267
|
+
'three sequential completions reuse same runtime and listener',
|
|
268
|
+
async () => {
|
|
269
|
+
// Sends A, waits for full completion (footer), sends B, waits for
|
|
270
|
+
// footer, sends C, waits for footer. Proves the listener stays alive
|
|
271
|
+
// across full run cycles without any interrupt/queue involvement.
|
|
272
|
+
// This is the "calm" path — no abort, no queue, just sequential use.
|
|
273
|
+
|
|
274
|
+
// 1. Send first message → thread created, session established
|
|
275
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
276
|
+
content: 'Reply with exactly: seq-alpha',
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
280
|
+
timeout: 4_000,
|
|
281
|
+
predicate: (t) => {
|
|
282
|
+
return t.name === 'Reply with exactly: seq-alpha'
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const th = discord.thread(thread.id)
|
|
287
|
+
|
|
288
|
+
// Wait for footer (italic project info line) — proves run A completed
|
|
289
|
+
await waitForBotMessageContaining({
|
|
290
|
+
discord,
|
|
291
|
+
threadId: thread.id,
|
|
292
|
+
userId: TEST_USER_ID,
|
|
293
|
+
text: '*project',
|
|
294
|
+
timeout: 4_000,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Capture runtime identity — should not change across runs
|
|
298
|
+
const runtimeAfterA = getRuntime(thread.id)
|
|
299
|
+
expect(runtimeAfterA).toBeDefined()
|
|
300
|
+
|
|
301
|
+
// 2. Send B after A fully completed
|
|
302
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
303
|
+
content: 'Reply with exactly: seq-beta',
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
await waitForBotReplyAfterUserMessage({
|
|
307
|
+
discord,
|
|
308
|
+
threadId: thread.id,
|
|
309
|
+
userId: TEST_USER_ID,
|
|
310
|
+
userMessageIncludes: 'seq-beta',
|
|
311
|
+
timeout: 4_000,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Wait for B's footer
|
|
315
|
+
await waitForBotMessageContaining({
|
|
316
|
+
discord,
|
|
317
|
+
threadId: thread.id,
|
|
318
|
+
userId: TEST_USER_ID,
|
|
319
|
+
text: '*project',
|
|
320
|
+
afterUserMessageIncludes: 'seq-beta',
|
|
321
|
+
timeout: 4_000,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Same runtime instance — listener was not recreated
|
|
325
|
+
const runtimeAfterB = getRuntime(thread.id)
|
|
326
|
+
expect(runtimeAfterB).toBe(runtimeAfterA)
|
|
327
|
+
|
|
328
|
+
// 3. Send C after B fully completed
|
|
329
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
330
|
+
content: 'Reply with exactly: seq-gamma',
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
await waitForBotReplyAfterUserMessage({
|
|
334
|
+
discord,
|
|
335
|
+
threadId: thread.id,
|
|
336
|
+
userId: TEST_USER_ID,
|
|
337
|
+
userMessageIncludes: 'seq-gamma',
|
|
338
|
+
timeout: 4_000,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
await waitForBotMessageContaining({
|
|
342
|
+
discord,
|
|
343
|
+
threadId: thread.id,
|
|
344
|
+
userId: TEST_USER_ID,
|
|
345
|
+
text: '*project',
|
|
346
|
+
afterUserMessageIncludes: 'seq-gamma',
|
|
347
|
+
timeout: 4_000,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Still the same runtime — three full cycles, one runtime, one listener
|
|
351
|
+
const runtimeAfterC = getRuntime(thread.id)
|
|
352
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
353
|
+
"--- from: user (lifecycle-tester)
|
|
354
|
+
Reply with exactly: seq-alpha
|
|
355
|
+
--- from: assistant (TestBot)
|
|
356
|
+
⬥ ok
|
|
357
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
358
|
+
--- from: user (lifecycle-tester)
|
|
359
|
+
Reply with exactly: seq-beta
|
|
360
|
+
--- from: assistant (TestBot)
|
|
361
|
+
⬥ ok
|
|
362
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
363
|
+
--- from: user (lifecycle-tester)
|
|
364
|
+
Reply with exactly: seq-gamma
|
|
365
|
+
--- from: assistant (TestBot)
|
|
366
|
+
⬥ ok
|
|
367
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
368
|
+
`)
|
|
369
|
+
expect(runtimeAfterC).toBe(runtimeAfterA)
|
|
370
|
+
},
|
|
371
|
+
15_000,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
test(
|
|
375
|
+
'footer includes context percentage and model id',
|
|
376
|
+
async () => {
|
|
377
|
+
const prompt = 'Reply with exactly: footer-check'
|
|
378
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
379
|
+
content: prompt,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
383
|
+
timeout: 4_000,
|
|
384
|
+
predicate: (t) => {
|
|
385
|
+
return t.name === prompt
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
await waitForBotMessageContaining({
|
|
390
|
+
discord,
|
|
391
|
+
threadId: thread.id,
|
|
392
|
+
userId: TEST_USER_ID,
|
|
393
|
+
text: 'deterministic-v2',
|
|
394
|
+
timeout: 4_000,
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const messages = await discord.thread(thread.id).getMessages()
|
|
398
|
+
|
|
399
|
+
const footerMessage = messages.find((message) => {
|
|
400
|
+
if (message.author.id !== discord.botUserId) {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
if (!message.content.startsWith('*')) {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
return message.content.includes('deterministic-v2')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
410
|
+
"--- from: user (lifecycle-tester)
|
|
411
|
+
Reply with exactly: footer-check
|
|
412
|
+
--- from: assistant (TestBot)
|
|
413
|
+
⬥ ok
|
|
414
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
415
|
+
`)
|
|
416
|
+
expect(footerMessage).toBeDefined()
|
|
417
|
+
if (!footerMessage) {
|
|
418
|
+
throw new Error('Expected footer message to be present')
|
|
419
|
+
}
|
|
420
|
+
expect(footerMessage.content).toContain('deterministic-v2')
|
|
421
|
+
expect(footerMessage.content).toMatch(/\d+%/)
|
|
422
|
+
},
|
|
423
|
+
10_000,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
test(
|
|
427
|
+
'existing runtime reconnects after shared opencode server restart',
|
|
428
|
+
async () => {
|
|
429
|
+
const prompt = 'Reply with exactly: reconnect-alpha'
|
|
430
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
431
|
+
content: prompt,
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
435
|
+
timeout: 4_000,
|
|
436
|
+
predicate: (t) => {
|
|
437
|
+
return t.name === prompt
|
|
438
|
+
},
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const th = discord.thread(thread.id)
|
|
442
|
+
|
|
443
|
+
await waitForBotMessageContaining({
|
|
444
|
+
discord,
|
|
445
|
+
threadId: thread.id,
|
|
446
|
+
userId: TEST_USER_ID,
|
|
447
|
+
text: '*project',
|
|
448
|
+
timeout: 4_000,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const runtimeBeforeRestart = getRuntime(thread.id)
|
|
452
|
+
expect(runtimeBeforeRestart).toBeDefined()
|
|
453
|
+
|
|
454
|
+
const restartResult = await restartOpencodeServer()
|
|
455
|
+
if (restartResult instanceof Error) {
|
|
456
|
+
throw restartResult
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
460
|
+
content: 'Reply with exactly: reconnect-beta',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
await waitForBotReplyAfterUserMessage({
|
|
464
|
+
discord,
|
|
465
|
+
threadId: thread.id,
|
|
466
|
+
userId: TEST_USER_ID,
|
|
467
|
+
userMessageIncludes: 'reconnect-beta',
|
|
468
|
+
timeout: 4_000,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
await waitForBotMessageContaining({
|
|
472
|
+
discord,
|
|
473
|
+
threadId: thread.id,
|
|
474
|
+
userId: TEST_USER_ID,
|
|
475
|
+
text: '*project',
|
|
476
|
+
afterUserMessageIncludes: 'reconnect-beta',
|
|
477
|
+
timeout: 4_000,
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
481
|
+
"--- from: user (lifecycle-tester)
|
|
482
|
+
Reply with exactly: reconnect-alpha
|
|
483
|
+
--- from: assistant (TestBot)
|
|
484
|
+
⬥ ok
|
|
485
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
486
|
+
--- from: user (lifecycle-tester)
|
|
487
|
+
Reply with exactly: reconnect-beta
|
|
488
|
+
--- from: assistant (TestBot)
|
|
489
|
+
⬥ ok
|
|
490
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
491
|
+
`)
|
|
492
|
+
|
|
493
|
+
const runtimeAfterRestart = getRuntime(thread.id)
|
|
494
|
+
expect(runtimeAfterRestart).toBe(runtimeBeforeRestart)
|
|
495
|
+
},
|
|
496
|
+
15_000,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
test(
|
|
500
|
+
'does not print a context-usage notice for the final text part right before the footer',
|
|
501
|
+
async () => {
|
|
502
|
+
const prompt = 'Reply with exactly: footer-high-usage'
|
|
503
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
504
|
+
content: prompt,
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
508
|
+
timeout: 4_000,
|
|
509
|
+
predicate: (t) => {
|
|
510
|
+
return t.name === prompt
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
await waitForBotMessageContaining({
|
|
515
|
+
discord,
|
|
516
|
+
threadId: thread.id,
|
|
517
|
+
userId: TEST_USER_ID,
|
|
518
|
+
text: 'deterministic-v2',
|
|
519
|
+
timeout: 4_000,
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
523
|
+
"--- from: user (lifecycle-tester)
|
|
524
|
+
Reply with exactly: footer-high-usage
|
|
525
|
+
--- from: assistant (TestBot)
|
|
526
|
+
⬥ ok
|
|
527
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
528
|
+
`)
|
|
529
|
+
|
|
530
|
+
const threadText = await discord.thread(thread.id).text()
|
|
531
|
+
expect(threadText).not.toContain('⬦ context usage')
|
|
532
|
+
},
|
|
533
|
+
10_000,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
test(
|
|
537
|
+
'two near-simultaneous messages to same thread serialize correctly',
|
|
538
|
+
async () => {
|
|
539
|
+
// Sends A to create a thread, then fires B and C simultaneously into
|
|
540
|
+
// the thread (no await between them). Without the old threadMessageQueue,
|
|
541
|
+
// the runtime's dispatchAction must serialize these. Both should get
|
|
542
|
+
// responses and the thread should not deadlock or create duplicate sessions.
|
|
543
|
+
|
|
544
|
+
// 1. Establish thread + session
|
|
545
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
546
|
+
content: 'Reply with exactly: concurrent-setup',
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
550
|
+
timeout: 4_000,
|
|
551
|
+
predicate: (t) => {
|
|
552
|
+
return t.name === 'Reply with exactly: concurrent-setup'
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const th = discord.thread(thread.id)
|
|
557
|
+
const setupReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
558
|
+
expect(setupReply.content.trim().length).toBeGreaterThan(0)
|
|
559
|
+
|
|
560
|
+
// Wait for setup footer so the run is fully idle
|
|
561
|
+
await waitForBotMessageContaining({
|
|
562
|
+
discord,
|
|
563
|
+
threadId: thread.id,
|
|
564
|
+
userId: TEST_USER_ID,
|
|
565
|
+
text: '*project',
|
|
566
|
+
timeout: 4_000,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// Snapshot bot message count before sending concurrent messages
|
|
570
|
+
const beforeMessages = await th.getMessages()
|
|
571
|
+
const beforeBotCount = beforeMessages.filter((m) => {
|
|
572
|
+
return m.author.id === discord.botUserId
|
|
573
|
+
}).length
|
|
574
|
+
|
|
575
|
+
// 2. Fire B and C simultaneously — no await between sends
|
|
576
|
+
const sendB = th.user(TEST_USER_ID).sendMessage({
|
|
577
|
+
content: 'Reply with exactly: concurrent-bravo',
|
|
578
|
+
})
|
|
579
|
+
const sendC = th.user(TEST_USER_ID).sendMessage({
|
|
580
|
+
content: 'Reply with exactly: concurrent-charlie',
|
|
581
|
+
})
|
|
582
|
+
await Promise.all([sendB, sendC])
|
|
583
|
+
|
|
584
|
+
// 3. Both should eventually get bot replies — the runtime serializes them
|
|
585
|
+
await waitForBotReplyAfterUserMessage({
|
|
586
|
+
discord,
|
|
587
|
+
threadId: thread.id,
|
|
588
|
+
userId: TEST_USER_ID,
|
|
589
|
+
userMessageIncludes: 'concurrent-bravo',
|
|
590
|
+
timeout: 4_000,
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
await waitForBotReplyAfterUserMessage({
|
|
594
|
+
discord,
|
|
595
|
+
threadId: thread.id,
|
|
596
|
+
userId: TEST_USER_ID,
|
|
597
|
+
userMessageIncludes: 'concurrent-charlie',
|
|
598
|
+
timeout: 4_000,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// 4. Verify both user messages arrived and the thread didn't deadlock.
|
|
602
|
+
// With explicit abort flows, bravo can be aborted by charlie before
|
|
603
|
+
// producing a reply, so we can't assert +2 bot messages. What we
|
|
604
|
+
// CAN verify: both user messages exist, charlie (the last one) has
|
|
605
|
+
// a bot reply after it, and the replies are distinct messages.
|
|
606
|
+
// No inline snapshot here — the concurrent abort race makes message
|
|
607
|
+
// ordering nondeterministic (bravo may or may not get a reply).
|
|
608
|
+
const messages = await th.getMessages()
|
|
609
|
+
|
|
610
|
+
const bravoIndex = messages.findIndex((m) => {
|
|
611
|
+
return (
|
|
612
|
+
m.author.id === TEST_USER_ID &&
|
|
613
|
+
m.content.includes('concurrent-bravo')
|
|
614
|
+
)
|
|
615
|
+
})
|
|
616
|
+
const charlieIndex = messages.findIndex((m) => {
|
|
617
|
+
return (
|
|
618
|
+
m.author.id === TEST_USER_ID &&
|
|
619
|
+
m.content.includes('concurrent-charlie')
|
|
620
|
+
)
|
|
621
|
+
})
|
|
622
|
+
expect(bravoIndex).toBeGreaterThan(-1)
|
|
623
|
+
expect(charlieIndex).toBeGreaterThan(-1)
|
|
624
|
+
expect(bravoIndex).toBeLessThan(charlieIndex)
|
|
625
|
+
|
|
626
|
+
// Charlie (the last queued message) must have a bot reply after it.
|
|
627
|
+
const charlieReplyIndex = messages.findIndex((m, i) => {
|
|
628
|
+
return i > charlieIndex && m.author.id === discord.botUserId
|
|
629
|
+
})
|
|
630
|
+
expect(charlieReplyIndex).toBeGreaterThan(-1)
|
|
631
|
+
|
|
632
|
+
// At least 1 new bot message appeared (charlie's reply). If bravo
|
|
633
|
+
// wasn't aborted, there will be 2. Either way, no deadlock.
|
|
634
|
+
const afterBotCount = messages.filter((m) => {
|
|
635
|
+
return m.author.id === discord.botUserId
|
|
636
|
+
}).length
|
|
637
|
+
expect(afterBotCount).toBeGreaterThanOrEqual(beforeBotCount + 1)
|
|
638
|
+
},
|
|
639
|
+
15_000,
|
|
640
|
+
)
|
|
641
|
+
})
|