@otto-assistant/otto 0.1.2 → 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/README.md +0 -142
- 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,264 @@
|
|
|
1
|
+
// Deterministic markdown export tests.
|
|
2
|
+
// Uses the shared opencode server manager with the deterministic provider,
|
|
3
|
+
// creates sessions with known content, and validates markdown output.
|
|
4
|
+
// No dependency on machine-local session state.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import url from 'node:url';
|
|
8
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
9
|
+
import * as errore from 'errore';
|
|
10
|
+
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
11
|
+
import { ShareMarkdown, getCompactSessionContext } from './markdown.js';
|
|
12
|
+
import { setDataDir } from './config.js';
|
|
13
|
+
import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js';
|
|
14
|
+
import { cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
15
|
+
const ROOT = path.resolve(process.cwd(), 'tmp', 'markdown-test');
|
|
16
|
+
function createRunDirectories() {
|
|
17
|
+
fs.mkdirSync(ROOT, { recursive: true });
|
|
18
|
+
const dataDir = fs.mkdtempSync(path.join(ROOT, 'data-'));
|
|
19
|
+
const projectDirectory = path.join(ROOT, 'project');
|
|
20
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
21
|
+
initTestGitRepo(projectDirectory);
|
|
22
|
+
return { dataDir, projectDirectory };
|
|
23
|
+
}
|
|
24
|
+
function createMatchers() {
|
|
25
|
+
const helloMatcher = {
|
|
26
|
+
id: 'hello-reply',
|
|
27
|
+
priority: 100,
|
|
28
|
+
when: { latestUserTextIncludes: 'hello markdown test' },
|
|
29
|
+
then: {
|
|
30
|
+
parts: [
|
|
31
|
+
{ type: 'stream-start', warnings: [] },
|
|
32
|
+
{ type: 'text-start', id: 'hello-text' },
|
|
33
|
+
{ type: 'text-delta', id: 'hello-text', delta: 'Hello! This is a deterministic markdown test response.' },
|
|
34
|
+
{ type: 'text-end', id: 'hello-text' },
|
|
35
|
+
{ type: 'finish', finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 8, totalTokens: 18 } },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const defaultMatcher = {
|
|
40
|
+
id: 'default-reply',
|
|
41
|
+
priority: 1,
|
|
42
|
+
then: {
|
|
43
|
+
parts: [
|
|
44
|
+
{ type: 'stream-start', warnings: [] },
|
|
45
|
+
{ type: 'text-start', id: 'default-text' },
|
|
46
|
+
{ type: 'text-delta', id: 'default-text', delta: 'ok' },
|
|
47
|
+
{ type: 'text-end', id: 'default-text' },
|
|
48
|
+
{ type: 'finish', finishReason: 'stop', usage: { inputTokens: 5, outputTokens: 1, totalTokens: 6 } },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
return [helloMatcher, defaultMatcher];
|
|
53
|
+
}
|
|
54
|
+
let client;
|
|
55
|
+
let directories;
|
|
56
|
+
let testStartTime;
|
|
57
|
+
let sessionID;
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
testStartTime = Date.now();
|
|
60
|
+
directories = createRunDirectories();
|
|
61
|
+
setDataDir(directories.dataDir);
|
|
62
|
+
const providerNpm = url
|
|
63
|
+
.pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
|
|
64
|
+
.toString();
|
|
65
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
66
|
+
providerName: 'deterministic-provider',
|
|
67
|
+
providerNpm,
|
|
68
|
+
model: 'deterministic-v2',
|
|
69
|
+
smallModel: 'deterministic-v2',
|
|
70
|
+
settings: {
|
|
71
|
+
strict: false,
|
|
72
|
+
matchers: createMatchers(),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
76
|
+
// Start the shared opencode server via otto's server manager
|
|
77
|
+
const getClient = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
78
|
+
if (getClient instanceof Error) {
|
|
79
|
+
throw getClient;
|
|
80
|
+
}
|
|
81
|
+
client = getClient();
|
|
82
|
+
// Create a session and send a known prompt
|
|
83
|
+
const createResult = await client.session.create({
|
|
84
|
+
directory: directories.projectDirectory,
|
|
85
|
+
title: 'Markdown Test Session',
|
|
86
|
+
});
|
|
87
|
+
sessionID = createResult.data.id;
|
|
88
|
+
// Send prompt and wait for completion (promptAsync returns immediately)
|
|
89
|
+
await client.session.promptAsync({
|
|
90
|
+
sessionID,
|
|
91
|
+
directory: directories.projectDirectory,
|
|
92
|
+
parts: [{ type: 'text', text: 'hello markdown test' }],
|
|
93
|
+
});
|
|
94
|
+
// Wait for assistant text parts to be fully written (not just message existence).
|
|
95
|
+
// The deterministic provider responds instantly but opencode writes parts
|
|
96
|
+
// asynchronously, so we must poll until non-empty text content appears.
|
|
97
|
+
// Under parallel test load the server is slower, so use generous timeouts.
|
|
98
|
+
const maxWait = 15_000;
|
|
99
|
+
const pollStart = Date.now();
|
|
100
|
+
while (Date.now() - pollStart < maxWait) {
|
|
101
|
+
const msgs = await client.session.messages({
|
|
102
|
+
sessionID,
|
|
103
|
+
directory: directories.projectDirectory,
|
|
104
|
+
});
|
|
105
|
+
const assistantMsg = msgs.data?.find((m) => m.info.role === 'assistant');
|
|
106
|
+
const hasTextParts = assistantMsg?.parts?.some((p) => {
|
|
107
|
+
return p.type === 'text' && p.text && !p.synthetic;
|
|
108
|
+
});
|
|
109
|
+
if (hasTextParts) {
|
|
110
|
+
// Extra wait for step-start and other parts to be flushed
|
|
111
|
+
await new Promise((resolve) => {
|
|
112
|
+
setTimeout(resolve, 500);
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => {
|
|
117
|
+
setTimeout(resolve, 200);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, 20_000);
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
if (directories) {
|
|
123
|
+
await cleanupTestSessions({
|
|
124
|
+
projectDirectory: directories.projectDirectory,
|
|
125
|
+
testStartTime,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
await stopOpencodeServer();
|
|
129
|
+
if (directories) {
|
|
130
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}, 5_000);
|
|
133
|
+
// Strip dynamic parts (timestamps, durations, branch names) for stable assertions
|
|
134
|
+
function normalizeMarkdown(md) {
|
|
135
|
+
return md
|
|
136
|
+
// Normalize "Completed in Xs" to a fixed string
|
|
137
|
+
.replace(/\*Completed in [\d.]+[ms]+\*/g, '*Completed in Xs*')
|
|
138
|
+
// Normalize "Duration: Xs" tool timing
|
|
139
|
+
.replace(/\*Duration: [\d.]+[ms]+\*/g, '*Duration: Xs*')
|
|
140
|
+
// Normalize ISO dates in session info
|
|
141
|
+
.replace(/\*\*Created\*\*: .+/g, '**Created**: <date>')
|
|
142
|
+
.replace(/\*\*Updated\*\*: .+/g, '**Updated**: <date>')
|
|
143
|
+
// Normalize opencode version
|
|
144
|
+
.replace(/\*\*OpenCode Version\*\*: v[\d.]+.*/g, '**OpenCode Version**: v<version>')
|
|
145
|
+
// Strip git branch context injected by opencode into user messages
|
|
146
|
+
.replace(/\[Current branch: [^\]]+\]\n?\n?/g, '')
|
|
147
|
+
.replace(/\[current git branch is [^\]]+\]\n?\n?/g, '')
|
|
148
|
+
.replace(/\[warning: repository is in detached HEAD[^\]]*\]\n?\n?/g, '');
|
|
149
|
+
}
|
|
150
|
+
test('generate markdown with system info', async () => {
|
|
151
|
+
const exporter = new ShareMarkdown(client);
|
|
152
|
+
const markdownResult = await exporter.generate({
|
|
153
|
+
sessionID,
|
|
154
|
+
includeSystemInfo: true,
|
|
155
|
+
});
|
|
156
|
+
expect(errore.isOk(markdownResult)).toBe(true);
|
|
157
|
+
const markdown = errore.unwrap(markdownResult);
|
|
158
|
+
expect(markdown).toContain('# Markdown Test Session');
|
|
159
|
+
expect(markdown).toContain('## Session Information');
|
|
160
|
+
expect(markdown).toContain('## Conversation');
|
|
161
|
+
expect(markdown).toContain('### 👤 User');
|
|
162
|
+
expect(markdown).toContain('hello markdown test');
|
|
163
|
+
expect(markdown).toContain('### 🤖 Assistant');
|
|
164
|
+
expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
|
|
165
|
+
expect(markdown).toContain('**Started using deterministic-provider/deterministic-v2**');
|
|
166
|
+
const normalized = normalizeMarkdown(markdown);
|
|
167
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
168
|
+
"# Markdown Test Session
|
|
169
|
+
|
|
170
|
+
## Session Information
|
|
171
|
+
|
|
172
|
+
- **Created**: <date>
|
|
173
|
+
- **Updated**: <date>
|
|
174
|
+
- **OpenCode Version**: v<version>
|
|
175
|
+
|
|
176
|
+
## Conversation
|
|
177
|
+
|
|
178
|
+
### 👤 User
|
|
179
|
+
|
|
180
|
+
hello markdown test
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
### 🤖 Assistant (deterministic-v2)
|
|
184
|
+
|
|
185
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
186
|
+
|
|
187
|
+
Hello! This is a deterministic markdown test response.
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
*Completed in Xs*
|
|
191
|
+
"
|
|
192
|
+
`);
|
|
193
|
+
});
|
|
194
|
+
test('generate markdown without system info', async () => {
|
|
195
|
+
const exporter = new ShareMarkdown(client);
|
|
196
|
+
const markdown = await exporter.generate({
|
|
197
|
+
sessionID,
|
|
198
|
+
includeSystemInfo: false,
|
|
199
|
+
});
|
|
200
|
+
expect(errore.isOk(markdown)).toBe(true);
|
|
201
|
+
const md = errore.unwrap(markdown);
|
|
202
|
+
expect(md).toContain('# Markdown Test Session');
|
|
203
|
+
expect(md).not.toContain('## Session Information');
|
|
204
|
+
expect(md).toContain('## Conversation');
|
|
205
|
+
const normalized = normalizeMarkdown(md);
|
|
206
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
207
|
+
"# Markdown Test Session
|
|
208
|
+
|
|
209
|
+
## Conversation
|
|
210
|
+
|
|
211
|
+
### 👤 User
|
|
212
|
+
|
|
213
|
+
hello markdown test
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
### 🤖 Assistant (deterministic-v2)
|
|
217
|
+
|
|
218
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
219
|
+
|
|
220
|
+
Hello! This is a deterministic markdown test response.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
*Completed in Xs*
|
|
224
|
+
"
|
|
225
|
+
`);
|
|
226
|
+
});
|
|
227
|
+
test('error handling for non-existent session', async () => {
|
|
228
|
+
const exporter = new ShareMarkdown(client);
|
|
229
|
+
const badSessionID = 'ses_nonexistent_' + Date.now();
|
|
230
|
+
const result = await exporter.generate({ sessionID: badSessionID });
|
|
231
|
+
expect(result).toBeInstanceOf(Error);
|
|
232
|
+
expect(result.message).toContain(`Session ${badSessionID} not found`);
|
|
233
|
+
});
|
|
234
|
+
test('getCompactSessionContext generates compact format', async () => {
|
|
235
|
+
const contextResult = await getCompactSessionContext({
|
|
236
|
+
client,
|
|
237
|
+
sessionId: sessionID,
|
|
238
|
+
includeSystemPrompt: false,
|
|
239
|
+
maxMessages: 10,
|
|
240
|
+
});
|
|
241
|
+
expect(errore.isOk(contextResult)).toBe(true);
|
|
242
|
+
const context = errore.unwrap(contextResult);
|
|
243
|
+
expect(context).toBeTruthy();
|
|
244
|
+
// User text may be prefixed with branch context injected by opencode
|
|
245
|
+
expect(context).toContain('hello markdown test');
|
|
246
|
+
expect(context).toContain('[User]:');
|
|
247
|
+
expect(context).toContain('[Assistant]:');
|
|
248
|
+
expect(context).toContain('Hello! This is a deterministic markdown test response.');
|
|
249
|
+
expect(context).not.toContain('[System Prompt]');
|
|
250
|
+
});
|
|
251
|
+
test('generate markdown with lastAssistantOnly', async () => {
|
|
252
|
+
const exporter = new ShareMarkdown(client);
|
|
253
|
+
const markdownResult = await exporter.generate({
|
|
254
|
+
sessionID,
|
|
255
|
+
lastAssistantOnly: true,
|
|
256
|
+
});
|
|
257
|
+
expect(errore.isOk(markdownResult)).toBe(true);
|
|
258
|
+
const markdown = errore.unwrap(markdownResult);
|
|
259
|
+
// lastAssistantOnly should NOT include title header or conversation section header
|
|
260
|
+
expect(markdown).not.toContain('# Markdown Test Session');
|
|
261
|
+
expect(markdown).not.toContain('## Conversation');
|
|
262
|
+
// Should contain the assistant response
|
|
263
|
+
expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
|
|
264
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// OpenCode plugin that snapshots the MEMORY.md heading overview once per
|
|
2
|
+
// session and injects that frozen snapshot on the first real user message.
|
|
3
|
+
// The snapshot is cached by session ID so later MEMORY.md edits do not change
|
|
4
|
+
// the prompt for the same session and do not invalidate OpenCode's cache.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import * as errore from 'errore';
|
|
9
|
+
import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
10
|
+
import { condenseMemoryMd } from './condense-memory.js';
|
|
11
|
+
import { initSentry, notifyError } from './sentry.js';
|
|
12
|
+
const logger = createPluginLogger('OPENCODE');
|
|
13
|
+
function createSessionState() {
|
|
14
|
+
return {
|
|
15
|
+
hasFrozenOverview: false,
|
|
16
|
+
frozenOverviewText: null,
|
|
17
|
+
injected: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildMemoryOverviewReminder({ condensed }) {
|
|
21
|
+
// Trailing newline so this synthetic part does not fuse with the next text
|
|
22
|
+
// part when the model concatenates message parts.
|
|
23
|
+
return `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>\n`;
|
|
24
|
+
}
|
|
25
|
+
async function freezeMemoryOverview({ directory, state, }) {
|
|
26
|
+
if (state.hasFrozenOverview) {
|
|
27
|
+
return state.frozenOverviewText;
|
|
28
|
+
}
|
|
29
|
+
const memoryPath = path.join(directory, 'MEMORY.md');
|
|
30
|
+
const memoryContentResult = await fs.promises.readFile(memoryPath, 'utf-8').catch(() => {
|
|
31
|
+
return null;
|
|
32
|
+
});
|
|
33
|
+
if (!memoryContentResult) {
|
|
34
|
+
state.hasFrozenOverview = true;
|
|
35
|
+
state.frozenOverviewText = null;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const condensed = condenseMemoryMd(memoryContentResult);
|
|
39
|
+
state.hasFrozenOverview = true;
|
|
40
|
+
state.frozenOverviewText = buildMemoryOverviewReminder({ condensed });
|
|
41
|
+
return state.frozenOverviewText;
|
|
42
|
+
}
|
|
43
|
+
const memoryOverviewPlugin = async ({ directory }) => {
|
|
44
|
+
initSentry();
|
|
45
|
+
const dataDir = process.env.OTTO_DATA_DIR;
|
|
46
|
+
if (dataDir) {
|
|
47
|
+
setPluginLogFilePath(dataDir);
|
|
48
|
+
}
|
|
49
|
+
const sessions = new Map();
|
|
50
|
+
function getOrCreateSessionState({ sessionID }) {
|
|
51
|
+
const existing = sessions.get(sessionID);
|
|
52
|
+
if (existing) {
|
|
53
|
+
return existing;
|
|
54
|
+
}
|
|
55
|
+
const state = createSessionState();
|
|
56
|
+
sessions.set(sessionID, state);
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
'chat.message': async (input, output) => {
|
|
61
|
+
const result = await errore.tryAsync({
|
|
62
|
+
try: async () => {
|
|
63
|
+
const state = getOrCreateSessionState({ sessionID: input.sessionID });
|
|
64
|
+
if (state.injected) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const firstPart = output.parts.find((part) => {
|
|
68
|
+
if (part.type !== 'text') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return part.synthetic !== true;
|
|
72
|
+
});
|
|
73
|
+
if (!firstPart || firstPart.type !== 'text' || firstPart.text.trim().length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const overviewText = await freezeMemoryOverview({ directory, state });
|
|
77
|
+
state.injected = true;
|
|
78
|
+
if (!overviewText) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
output.parts.push({
|
|
82
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
83
|
+
sessionID: input.sessionID,
|
|
84
|
+
messageID: firstPart.messageID,
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: overviewText,
|
|
87
|
+
synthetic: true,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
catch: (error) => {
|
|
91
|
+
return new Error('memory overview chat.message hook failed', {
|
|
92
|
+
cause: error,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
if (!(result instanceof Error)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`);
|
|
100
|
+
void notifyError(result, 'memory overview plugin chat.message hook failed');
|
|
101
|
+
},
|
|
102
|
+
event: async ({ event }) => {
|
|
103
|
+
const result = await errore.tryAsync({
|
|
104
|
+
try: async () => {
|
|
105
|
+
if (event.type !== 'session.deleted') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const id = event.properties?.info?.id;
|
|
109
|
+
if (!id) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
sessions.delete(id);
|
|
113
|
+
},
|
|
114
|
+
catch: (error) => {
|
|
115
|
+
return new Error('memory overview event hook failed', {
|
|
116
|
+
cause: error,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
if (!(result instanceof Error)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`);
|
|
124
|
+
void notifyError(result, 'memory overview plugin event hook failed');
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
export { memoryOverviewPlugin };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// E2e test verifying that the opencode server populates the `finish` field
|
|
2
|
+
// on assistant messages. This field is critical for otto's footer logic:
|
|
3
|
+
// isAssistantMessageNaturalCompletion checks `message.finish !== 'tool-calls'`
|
|
4
|
+
// to suppress footers on intermediate tool-call steps.
|
|
5
|
+
// When `finish` is missing/null, every completed assistant message gets a
|
|
6
|
+
// spurious footer, breaking multi-step tool chains (16 test failures).
|
|
7
|
+
//
|
|
8
|
+
// Direct SDK test — no Discord layer needed since this is a server-level bug.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import url from 'node:url';
|
|
12
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
14
|
+
import { setDataDir } from './config.js';
|
|
15
|
+
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
16
|
+
import { cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
17
|
+
const ROOT = path.resolve(process.cwd(), 'tmp', 'finish-field-e2e');
|
|
18
|
+
function createRunDirectories() {
|
|
19
|
+
fs.mkdirSync(ROOT, { recursive: true });
|
|
20
|
+
const dataDir = fs.mkdtempSync(path.join(ROOT, 'data-'));
|
|
21
|
+
const projectDirectory = path.join(ROOT, 'project');
|
|
22
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
23
|
+
initTestGitRepo(projectDirectory);
|
|
24
|
+
return { dataDir, projectDirectory };
|
|
25
|
+
}
|
|
26
|
+
function createMatchers() {
|
|
27
|
+
// Tool-call step: finish="tool-calls"
|
|
28
|
+
const toolCallMatcher = {
|
|
29
|
+
id: 'finish-tool-call',
|
|
30
|
+
priority: 20,
|
|
31
|
+
when: {
|
|
32
|
+
lastMessageRole: 'user',
|
|
33
|
+
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
34
|
+
},
|
|
35
|
+
then: {
|
|
36
|
+
parts: [
|
|
37
|
+
{ type: 'stream-start', warnings: [] },
|
|
38
|
+
{ type: 'text-start', id: 'ft' },
|
|
39
|
+
{ type: 'text-delta', id: 'ft', delta: 'calling tool' },
|
|
40
|
+
{ type: 'text-end', id: 'ft' },
|
|
41
|
+
{
|
|
42
|
+
type: 'tool-call',
|
|
43
|
+
toolCallId: 'finish-bash',
|
|
44
|
+
toolName: 'bash',
|
|
45
|
+
input: JSON.stringify({ command: 'echo ok', description: 'test' }),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'finish',
|
|
49
|
+
finishReason: 'tool-calls',
|
|
50
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
// Follow-up after tool result: finish="stop"
|
|
56
|
+
const followupMatcher = {
|
|
57
|
+
id: 'finish-followup',
|
|
58
|
+
priority: 21,
|
|
59
|
+
when: {
|
|
60
|
+
lastMessageRole: 'tool',
|
|
61
|
+
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
62
|
+
},
|
|
63
|
+
then: {
|
|
64
|
+
parts: [
|
|
65
|
+
{ type: 'stream-start', warnings: [] },
|
|
66
|
+
{ type: 'text-start', id: 'ff' },
|
|
67
|
+
{ type: 'text-delta', id: 'ff', delta: 'tool done' },
|
|
68
|
+
{ type: 'text-end', id: 'ff' },
|
|
69
|
+
{
|
|
70
|
+
type: 'finish',
|
|
71
|
+
finishReason: 'stop',
|
|
72
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return [toolCallMatcher, followupMatcher];
|
|
78
|
+
}
|
|
79
|
+
let client;
|
|
80
|
+
let directories;
|
|
81
|
+
let testStartTime;
|
|
82
|
+
beforeAll(async () => {
|
|
83
|
+
testStartTime = Date.now();
|
|
84
|
+
directories = createRunDirectories();
|
|
85
|
+
setDataDir(directories.dataDir);
|
|
86
|
+
const providerNpm = url
|
|
87
|
+
.pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
|
|
88
|
+
.toString();
|
|
89
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
90
|
+
providerName: 'deterministic-provider',
|
|
91
|
+
providerNpm,
|
|
92
|
+
model: 'deterministic-v2',
|
|
93
|
+
smallModel: 'deterministic-v2',
|
|
94
|
+
settings: { strict: false, matchers: createMatchers() },
|
|
95
|
+
});
|
|
96
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
97
|
+
const getClient = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
98
|
+
if (getClient instanceof Error) {
|
|
99
|
+
throw getClient;
|
|
100
|
+
}
|
|
101
|
+
client = getClient();
|
|
102
|
+
}, 20_000);
|
|
103
|
+
afterAll(async () => {
|
|
104
|
+
await cleanupTestSessions({
|
|
105
|
+
projectDirectory: directories.projectDirectory,
|
|
106
|
+
testStartTime,
|
|
107
|
+
});
|
|
108
|
+
await stopOpencodeServer();
|
|
109
|
+
}, 5_000);
|
|
110
|
+
test('tool-call step has finish="tool-calls", follow-up has finish="stop"', async () => {
|
|
111
|
+
const session = await client.session.create({
|
|
112
|
+
directory: directories.projectDirectory,
|
|
113
|
+
title: 'finish-field-test',
|
|
114
|
+
});
|
|
115
|
+
const sessionID = session.data.id;
|
|
116
|
+
await client.session.promptAsync({
|
|
117
|
+
sessionID,
|
|
118
|
+
directory: directories.projectDirectory,
|
|
119
|
+
parts: [{ type: 'text', text: 'FINISH_FIELD_TOOLCALL' }],
|
|
120
|
+
});
|
|
121
|
+
// Poll until we have 2 completed assistant messages (tool-call + follow-up)
|
|
122
|
+
const maxWait = 8_000;
|
|
123
|
+
const pollStart = Date.now();
|
|
124
|
+
let completedAssistants = [];
|
|
125
|
+
while (Date.now() - pollStart < maxWait) {
|
|
126
|
+
const msgs = await client.session.messages({
|
|
127
|
+
sessionID,
|
|
128
|
+
directory: directories.projectDirectory,
|
|
129
|
+
});
|
|
130
|
+
completedAssistants = (msgs.data || [])
|
|
131
|
+
.filter((m) => {
|
|
132
|
+
return m.info.role === 'assistant' && m.info.time.completed;
|
|
133
|
+
})
|
|
134
|
+
.map((m) => {
|
|
135
|
+
return {
|
|
136
|
+
finish: m.info.finish ?? null,
|
|
137
|
+
partTypes: m.parts.map((p) => { return p.type; }),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
if (completedAssistants.length >= 2) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
await new Promise((resolve) => { setTimeout(resolve, 100); });
|
|
144
|
+
}
|
|
145
|
+
// Snapshot completed assistant messages — finish should NOT be null
|
|
146
|
+
expect(completedAssistants).toMatchInlineSnapshot(`
|
|
147
|
+
[
|
|
148
|
+
{
|
|
149
|
+
"finish": "tool-calls",
|
|
150
|
+
"partTypes": [
|
|
151
|
+
"step-start",
|
|
152
|
+
"text",
|
|
153
|
+
"step-finish",
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"finish": "stop",
|
|
158
|
+
"partTypes": [
|
|
159
|
+
"step-start",
|
|
160
|
+
"text",
|
|
161
|
+
"step-finish",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
`);
|
|
166
|
+
const finishes = completedAssistants.map((m) => { return m.finish; });
|
|
167
|
+
expect(finishes).toEqual(['tool-calls', 'stop']);
|
|
168
|
+
}, 5_000);
|