discoclaw 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.context/dev.md +2 -2
  2. package/.context/pa.md +20 -11
  3. package/.context/runtime.md +36 -1
  4. package/.context/voice.md +3 -0
  5. package/.env.example +17 -1
  6. package/.env.example.full +88 -9
  7. package/README.md +36 -12
  8. package/dist/cli/init-wizard.js +22 -0
  9. package/dist/cli/init-wizard.test.js +47 -0
  10. package/dist/cold-storage/chunker.js +140 -0
  11. package/dist/cold-storage/chunker.test.js +141 -0
  12. package/dist/cold-storage/embeddings.js +59 -0
  13. package/dist/cold-storage/embeddings.test.js +172 -0
  14. package/dist/cold-storage/index.js +59 -0
  15. package/dist/cold-storage/index.test.js +131 -0
  16. package/dist/cold-storage/openai-compat.js +62 -0
  17. package/dist/cold-storage/openai-compat.test.js +129 -0
  18. package/dist/cold-storage/prompt-section.js +64 -0
  19. package/dist/cold-storage/prompt-section.test.js +107 -0
  20. package/dist/cold-storage/store.js +246 -0
  21. package/dist/cold-storage/store.test.js +376 -0
  22. package/dist/cold-storage/types.js +8 -0
  23. package/dist/config.js +96 -5
  24. package/dist/config.test.js +161 -3
  25. package/dist/cron/executor.js +8 -0
  26. package/dist/cron/executor.test.js +59 -2
  27. package/dist/discord/action-categories.js +2 -0
  28. package/dist/discord/actions-config.js +125 -61
  29. package/dist/discord/actions-config.test.js +36 -4
  30. package/dist/discord/actions-defer.js +20 -2
  31. package/dist/discord/actions-defer.test.js +270 -2
  32. package/dist/discord/actions-forge.js +192 -8
  33. package/dist/discord/actions-forge.test.js +91 -7
  34. package/dist/discord/actions-memory.js +38 -4
  35. package/dist/discord/actions-memory.test.js +88 -5
  36. package/dist/discord/actions-messaging.js +71 -6
  37. package/dist/discord/actions-messaging.test.js +280 -6
  38. package/dist/discord/actions-plan.js +266 -105
  39. package/dist/discord/actions-plan.test.js +59 -6
  40. package/dist/discord/actions-spawn.js +117 -18
  41. package/dist/discord/actions-spawn.test.js +609 -8
  42. package/dist/discord/actions.js +252 -74
  43. package/dist/discord/actions.test.js +85 -3
  44. package/dist/discord/audit-handler.js +32 -2
  45. package/dist/discord/audit-handler.test.js +71 -0
  46. package/dist/discord/cold-storage-ingest.js +93 -0
  47. package/dist/discord/cold-storage-ingest.test.js +220 -0
  48. package/dist/discord/defer-scheduler.js +36 -6
  49. package/dist/discord/deferred-runner.js +54 -10
  50. package/dist/discord/deferred-runner.test.js +240 -2
  51. package/dist/discord/durable-memory.js +117 -6
  52. package/dist/discord/durable-memory.test.js +264 -1
  53. package/dist/discord/forge-auto-implement.js +3 -0
  54. package/dist/discord/forge-auto-implement.test.js +12 -0
  55. package/dist/discord/forge-commands.js +446 -197
  56. package/dist/discord/forge-commands.test.js +642 -50
  57. package/dist/discord/forge-plan-registry.js +36 -7
  58. package/dist/discord/forge-plan-registry.test.js +87 -12
  59. package/dist/discord/health-command.js +7 -1
  60. package/dist/discord/health-command.test.js +82 -0
  61. package/dist/discord/image-download.js +14 -2
  62. package/dist/discord/image-download.test.js +42 -0
  63. package/dist/discord/long-run-watchdog.js +398 -0
  64. package/dist/discord/long-run-watchdog.test.js +290 -0
  65. package/dist/discord/memory-commands.js +48 -6
  66. package/dist/discord/memory-commands.test.js +84 -1
  67. package/dist/discord/memory-timing.integration.test.js +318 -5
  68. package/dist/discord/message-coordinator.js +752 -132
  69. package/dist/discord/message-coordinator.plan-run.test.js +241 -12
  70. package/dist/discord/message-coordinator.reaction-action-ordering.test.js +72 -13
  71. package/dist/discord/message-coordinator.reaction-cleanup.test.js +86 -3
  72. package/dist/discord/models-command.js +5 -5
  73. package/dist/discord/output-utils.js +129 -6
  74. package/dist/discord/phase-status-heartbeat.js +248 -0
  75. package/dist/discord/phase-status-heartbeat.test.js +126 -0
  76. package/dist/discord/plan-commands.js +220 -22
  77. package/dist/discord/plan-commands.test.js +383 -2
  78. package/dist/discord/plan-manager.js +415 -29
  79. package/dist/discord/plan-manager.test.js +464 -14
  80. package/dist/discord/plan-parser.js +8 -1
  81. package/dist/discord/plan-parser.test.js +25 -0
  82. package/dist/discord/prompt-common.js +287 -26
  83. package/dist/discord/prompt-common.test.js +616 -9
  84. package/dist/discord/reaction-handler.js +227 -35
  85. package/dist/discord/reaction-handler.test.js +439 -32
  86. package/dist/discord/runtime-event-text-adapter.js +152 -0
  87. package/dist/discord/runtime-event-text-adapter.test.js +241 -0
  88. package/dist/discord/runtime-signal-budget.js +172 -0
  89. package/dist/discord/runtime-signal-budget.test.js +68 -0
  90. package/dist/discord/runtime-utils.js +25 -3
  91. package/dist/discord/runtime-utils.test.js +59 -0
  92. package/dist/discord/shutdown-context.js +48 -0
  93. package/dist/discord/shutdown-context.test.js +283 -1
  94. package/dist/discord/spawn-registry.js +49 -0
  95. package/dist/discord/spawn-registry.test.js +90 -0
  96. package/dist/discord/status-channel.js +32 -1
  97. package/dist/discord/status-channel.test.js +98 -1
  98. package/dist/discord/status-command.js +8 -0
  99. package/dist/discord/status-command.test.js +18 -0
  100. package/dist/discord/streaming-progress.js +79 -5
  101. package/dist/discord/streaming-progress.test.js +379 -5
  102. package/dist/discord/summarizer-recency.test.js +130 -0
  103. package/dist/discord/summarizer.js +126 -6
  104. package/dist/discord/summarizer.test.js +111 -1
  105. package/dist/discord/tool-aware-queue.js +32 -6
  106. package/dist/discord/tool-aware-queue.test.js +67 -0
  107. package/dist/discord/update-command.js +1 -2
  108. package/dist/discord-followup.test.js +276 -2
  109. package/dist/discord.prompt-context.test.js +181 -12
  110. package/dist/discord.render.test.js +107 -3
  111. package/dist/image/resize.js +47 -0
  112. package/dist/image/resize.test.js +145 -0
  113. package/dist/index.js +545 -133
  114. package/dist/index.post-connect.js +4 -0
  115. package/dist/index.runtime.js +35 -3
  116. package/dist/index.runtime.test.js +263 -0
  117. package/dist/instructions/system-defaults.js +57 -0
  118. package/dist/instructions/system-defaults.test.js +94 -0
  119. package/dist/instructions/tracked-tools.js +59 -0
  120. package/dist/instructions/tracked-tools.test.js +89 -0
  121. package/dist/model-config.js +166 -0
  122. package/dist/model-config.test.js +276 -0
  123. package/dist/npm-managed.js +0 -1
  124. package/dist/npm-managed.test.js +0 -1
  125. package/dist/pipeline/engine.js +18 -1
  126. package/dist/pipeline/engine.test.js +88 -2
  127. package/dist/runtime/anthropic-rest.js +177 -0
  128. package/dist/runtime/anthropic-rest.test.js +337 -0
  129. package/dist/runtime/claude-code-cli.test.js +114 -0
  130. package/dist/runtime/cli-adapter.js +530 -371
  131. package/dist/runtime/cli-adapter.test.js +67 -0
  132. package/dist/runtime/cli-shared.js +2 -1
  133. package/dist/runtime/cli-shared.test.js +5 -0
  134. package/dist/runtime/codex-cli.js +5 -1
  135. package/dist/runtime/codex-cli.test.js +494 -4
  136. package/dist/runtime/concurrency-limit.test.js +52 -0
  137. package/dist/runtime/gemini-cli.test.js +21 -3
  138. package/dist/runtime/global-supervisor.js +382 -0
  139. package/dist/runtime/global-supervisor.test.js +301 -0
  140. package/dist/runtime/long-running-process.js +156 -1
  141. package/dist/runtime/long-running-process.test.js +74 -0
  142. package/dist/runtime/model-smoke-helpers.js +2 -2
  143. package/dist/runtime/model-tiers.js +25 -3
  144. package/dist/runtime/model-tiers.test.js +49 -12
  145. package/dist/runtime/openai-compat.js +16 -2
  146. package/dist/runtime/openai-compat.test.js +114 -2
  147. package/dist/runtime/openai-tool-exec.js +1207 -5
  148. package/dist/runtime/openai-tool-exec.test.js +535 -1
  149. package/dist/runtime/openai-tool-schemas.js +211 -14
  150. package/dist/runtime/openai-tool-schemas.test.js +59 -4
  151. package/dist/runtime/process-pool.js +29 -5
  152. package/dist/runtime/process-pool.test.js +27 -0
  153. package/dist/runtime/session-scanner.js +24 -2
  154. package/dist/runtime/session-scanner.test.js +5 -1
  155. package/dist/runtime/strategies/claude-strategy.js +108 -7
  156. package/dist/runtime/strategies/codex-strategy.js +220 -13
  157. package/dist/runtime/tools/fs-glob.js +92 -7
  158. package/dist/runtime/tools/fs-glob.test.js +76 -1
  159. package/dist/runtime/tools/path-security.js +7 -0
  160. package/dist/runtime-overrides.js +2 -10
  161. package/dist/runtime-overrides.test.js +15 -81
  162. package/dist/tasks/task-action-mutations.js +12 -5
  163. package/dist/tasks/task-action-thread-sync.js +4 -0
  164. package/dist/voice/voice-prompt-builder.js +36 -3
  165. package/dist/voice/voice-prompt-builder.test.js +60 -9
  166. package/dist/voice/voice-responder.js +31 -7
  167. package/dist/voice/voice-responder.test.js +10 -8
  168. package/dist/voice/voice-sanitize.js +47 -0
  169. package/dist/voice/voice-sanitize.test.js +30 -1
  170. package/dist/webhook/server.js +4 -2
  171. package/dist/webhook/server.test.js +91 -3
  172. package/dist/workspace-bootstrap.js +100 -13
  173. package/dist/workspace-bootstrap.test.js +283 -115
  174. package/dist/workspace-permissions.js +6 -4
  175. package/dist/workspace-permissions.test.js +9 -2
  176. package/package.json +14 -4
  177. package/templates/instructions/SYSTEM_DEFAULTS.md +102 -0
  178. package/templates/instructions/TOOLS.md +143 -0
  179. package/templates/workspace/AGENTS.md +14 -205
  180. package/templates/workspace/DISCOCLAW.md +15 -0
  181. package/templates/workspace/TOOLS.md +10 -496
package/.context/dev.md CHANGED
@@ -79,12 +79,12 @@ Two setup paths:
79
79
  | Variable | Default | Description |
80
80
  |----------|---------|-------------|
81
81
  | `PRIMARY_RUNTIME` | `claude` | Runtime engine (`claude`, `openai`, `openrouter`, `gemini`, `codex`) |
82
- | `RUNTIME_MODEL` | `capable` | Model tier (`fast`, `capable`) or concrete model name passed to the CLI |
82
+ | `RUNTIME_MODEL` | `capable` | **Deprecated — use `models.json` instead.** Model tier (`fast`, `capable`) or concrete model name passed to the CLI |
83
83
  | `RUNTIME_TOOLS` | `Bash,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch` | Comma-separated tool list |
84
84
  | `RUNTIME_TIMEOUT_MS` | `1800000` | Per-invocation timeout in milliseconds |
85
85
  | `RUNTIME_FALLBACK_MODEL` | *(unset)* | Auto-fallback model when primary is overloaded (e.g. `sonnet`) |
86
86
  | `RUNTIME_MAX_BUDGET_USD` | *(unset)* | Max USD per CLI process; one-shot = per invocation, multi-turn = per session lifetime |
87
- | `DISCOCLAW_FAST_MODEL` | `fast` | Default "fast" model tier alias used for summarization, auto-tag, and cron parsing |
87
+ | `DISCOCLAW_FAST_MODEL` | `fast` | **Deprecated — use `models.json` instead.** Default "fast" model tier alias used for summarization, auto-tag, and cron parsing |
88
88
  | `DISCOCLAW_RUNTIME_SESSIONS` | `1` | Persist Claude session IDs across messages |
89
89
  | `DISCOCLAW_SESSION_SCANNING` | `1` | Enable session ID scanning for resume detection |
90
90
  | `DISCOCLAW_ACTION_FOLLOWUP_DEPTH` | `3` | Max depth for chained action follow-ups |
package/.context/pa.md CHANGED
@@ -9,17 +9,20 @@ For architecture details, see `.context/architecture.md`.
9
9
 
10
10
  ## Workspace Files
11
11
 
12
- | File | Purpose | Loaded |
13
- |------|---------|--------|
14
- | `SOUL.md` | Core personality and values | Every prompt |
15
- | `IDENTITY.md` | Name and vibe | Every prompt |
16
- | `USER.md` | Who you're helping | Every prompt |
17
- | `AGENTS.md` | Your personal rules and conventions | Every prompt |
18
- | `TOOLS.md` | Available tools and integrations | Every prompt |
19
- | `MEMORY.md` | Curated long-term memory | DM prompts |
20
- | `BOOTSTRAP.md` | First-run onboarding (deleted after) | Once |
12
+ | File | Purpose | Owner | Loaded |
13
+ |------|---------|-------|--------|
14
+ | `SOUL.md` | Core personality and values | User | Every prompt |
15
+ | `IDENTITY.md` | Name and vibe | User | Every prompt |
16
+ | `USER.md` | Who you're helping | User | Every prompt |
17
+ | `templates/instructions/SYSTEM_DEFAULTS.md` | Tracked default instructions (runtime-injected) | Discoclaw repo (tracked) | Every prompt |
18
+ | `AGENTS.md` | Personal rules and preferences | User (never overwritten) | Every prompt |
19
+ | `TOOLS.md` | Available tools and integrations | Discoclaw | Every prompt |
20
+ | `MEMORY.md` | Curated long-term memory | User | DM prompts |
21
+ | `BOOTSTRAP.md` | First-run onboarding (deleted after) | User | Once |
21
22
 
22
23
  Templates live in `templates/workspace/` and are scaffolded on first run (copy-if-missing).
24
+ Tracked defaults come from `templates/instructions/SYSTEM_DEFAULTS.md` and are injected at runtime.
25
+ Legacy `workspace/DISCOCLAW.md` files are not authoritative.
23
26
 
24
27
  ## Operational Essentials
25
28
 
@@ -115,5 +118,11 @@ See `.context/memory.md` for full architecture, examples, and config reference.
115
118
 
116
119
  ## Customization
117
120
 
118
- These rules are generic defaults. Override or extend them in `workspace/AGENTS.md`,
119
- which is your personal space — not tracked by git, not overwritten on updates.
121
+ Instruction precedence is deterministic:
122
+ 1. immutable security policy (`ROOT_POLICY`)
123
+ 2. tracked defaults (`templates/instructions/SYSTEM_DEFAULTS.md`)
124
+ 3. `workspace/AGENTS.md` overrides
125
+ 4. memory/context sections
126
+
127
+ Customize behavior in `workspace/AGENTS.md` (user-owned, never overwritten).
128
+ Do not rely on `workspace/DISCOCLAW.md`; defaults are sourced from the tracked template and injected at runtime.
@@ -45,7 +45,7 @@ The factory provides: subprocess tracking, process pool, stall detection, sessio
45
45
  | Strategy | File | Multi-turn | Notes |
46
46
  |----------|------|------------|-------|
47
47
  | Claude Code | `strategies/claude-strategy.ts` | process-pool | Default JSONL parsing, image support |
48
- | Codex CLI | `strategies/codex-strategy.ts` | session-resume | Custom JSONL (thread.started, item.completed), error sanitization; reasoning items surface in the Discord preview during streaming but are excluded from the final reply |
48
+ | Codex CLI | `strategies/codex-strategy.ts` | session-resume | Custom JSONL (thread.started, item.completed), error sanitization; reasoning items surface in the Discord preview during streaming but are excluded from the final reply; image support via `--image` temp files |
49
49
  | Gemini CLI | `strategies/gemini-strategy.ts` | none (Phase 1) | Text-only output mode; no sessions; stdin fallback for large prompts |
50
50
  | Template | `strategies/template-strategy.ts` | — | Commented starting point for new models |
51
51
 
@@ -102,6 +102,39 @@ Shutdown: `killAllSubprocesses()` from `cli-adapter.ts` kills all tracked subpro
102
102
  - No tool execution, no fs tools
103
103
  - No image input/output support
104
104
 
105
+ ## Anthropic REST Runtime
106
+
107
+ Direct HTTP adapter for the Anthropic Messages API — no CLI subprocess, no cold-start. Designed for latency-sensitive paths like voice where the ~2-4 s CLI bootstrap is unacceptable.
108
+
109
+ - Adapter: `src/runtime/anthropic-rest.ts`
110
+ - Factory: `createAnthropicRestRuntime(opts)`
111
+ - Auth: `x-api-key` header (from `ANTHROPIC_API_KEY` env var)
112
+ - Streaming: SSE (`stream: true`) — emits `text_delta`, `usage`, `text_final`, `done` engine events
113
+ - Runtime ID: `claude_code` (same as CLI adapter so model tier resolution is compatible)
114
+ - Default model: `claude-sonnet-4-6` (set at registration time in `src/index.ts`)
115
+ - Capabilities: `streaming_text` only (no tools, no sessions)
116
+ - Conditional registration: only registered as `'anthropic'` in the runtime registry when `ANTHROPIC_API_KEY` is set
117
+
118
+ Env vars:
119
+
120
+ | Var | Default | Purpose |
121
+ |-----|---------|---------|
122
+ | `ANTHROPIC_API_KEY` | *(required)* | API key; also gates adapter registration |
123
+
124
+ Configurable via `AnthropicRestOpts`: `baseUrl` (default `https://api.anthropic.com`), `apiVersion` (default `2023-06-01`), `defaultMaxTokens` (default `1024`).
125
+
126
+ ### Voice auto-wiring
127
+
128
+ When both `ANTHROPIC_API_KEY` and `DISCOCLAW_VOICE_ENABLED=1` are set, the startup path in `src/index.ts` auto-wires the Anthropic REST adapter as the voice runtime. `resolveVoiceRuntime()` checks `voiceModelRef.runtime` first, then falls back to the `'anthropic'` registry entry, then the primary CLI runtime. Model overrides are now configured in `models.json`; the voice runtime override is still in `runtime-overrides.json` (`voiceRuntime` key). The model can also be changed via the `!models` command.
129
+
130
+ ### Key files
131
+
132
+ | File | Role |
133
+ |------|------|
134
+ | `src/runtime/anthropic-rest.ts` | Adapter: SSE streaming, abort/timeout, system prompt extraction |
135
+ | `src/runtime/anthropic-rest.test.ts` | Unit tests (mocked fetch, SSE parsing, error handling, abort) |
136
+ | `src/runtime/openai-compat.ts` | Provides `splitSystemPrompt()` used by the adapter |
137
+
105
138
  ## OpenRouter Adapter
106
139
 
107
140
  - Implementation: reuses `src/runtime/openai-compat.ts` with `id: 'openrouter'` — no separate adapter file needed.
@@ -279,6 +312,8 @@ When a Discord message or reaction target has image attachments (PNG, JPEG, WebP
279
312
  3. **Download** — `downloadAttachment()` fetches the image with a 10 s timeout, post-checks actual size, and returns base64.
280
313
  4. **Delivery** — The runtime adapter writes a `stream-json` stdin message containing `[{ type: 'text', text: prompt }, { type: 'image', source: { type: 'base64', ... } }, ...]`. When images are present, `--output-format` is forced to `stream-json` regardless of the configured format.
281
314
 
315
+ **Codex delivery:** Codex CLI does not accept base64 image data via stdin — it requires file paths on disk via `--image <path>` flags. The Codex strategy writes each `ImageData` (base64) to a temp file before invocation and cleans up after. The full pipeline is: Discord attachment → base64 `ImageData` → temp file → `--image <path>` → cleanup.
316
+
282
317
  ### Security controls
283
318
 
284
319
  | Control | Detail |
package/.context/voice.md CHANGED
@@ -51,6 +51,8 @@ User speaks in Discord voice channel
51
51
 
52
52
  - **Allowlist gating** — `AudioReceiver` only subscribes to users in `DISCORD_ALLOW_USER_IDS`. Empty allowlist = ignore everyone (fail-closed).
53
53
  - **Dual-flag voice actions** — Voice action execution requires both `VOICE_ENABLED` and `DISCORD_ACTIONS_VOICE`. The `buildVoiceActionFlags()` function intersects a voice-specific allowlist (messaging, tasks, memory) with env config; all other action categories are hard-disabled.
54
+ - **Queued invocations** — `VoiceResponder` queues new transcriptions when a pipeline is already in-flight instead of aborting the active AI call. Only the most recent pending text is kept (coalesced). On completion the responder drains the queue, processing the next pending transcription. This eliminates the death-spiral where CLI cold-start latency caused cascading cancellations. Barge-in still stops *playback* immediately but never cancels the running AI request.
55
+ - **Fast invoke path** — When `ANTHROPIC_API_KEY` is set, voice auto-wires to the Anthropic REST adapter (`src/runtime/anthropic-rest.ts`) instead of the CLI subprocess path. Direct HTTP eliminates the ~2-4 s CLI cold-start, bringing first-token latency under 500 ms. The wiring happens at startup in `src/index.ts`; at invoke time `resolveVoiceRuntime()` picks the `'anthropic'` adapter from the registry. Model configuration is now in `models.json`; the voice runtime override is still in `runtime-overrides.json` (`voiceRuntime` key). The model can also be changed via the `!models` command.
54
56
  - **Generation-based cancellation** — `VoiceResponder` increments a generation counter on each new transcription. If a newer transcription arrives mid-pipeline, the older one is silently abandoned.
55
57
  - **Barge-in** — Gated on a non-empty STT transcription result, not the raw VAD `speaking.start` event. Echo from the bot's own TTS leaking through the user's mic produces empty transcriptions and is ignored. Only when `VoiceResponder.handleTranscription()` receives a non-empty transcript while the player is active does it stop playback and advance the generation counter. This eliminates false positives from echo without relying on a static grace-period timeout.
56
58
  - **Conversation ring buffer** — `ConversationBuffer` maintains a per-guild 10-turn ring buffer of user/model exchanges that gets injected into the voice prompt as formatted conversation history. Turns are appended live during a session. On voice join, the buffer backfills from recent voice-log channel messages so context carries across disconnects. The buffer is cleared when the bot leaves the voice channel.
@@ -87,4 +89,5 @@ When `voiceEnabled=true`, the post-connect block in `src/index.ts` initializes t
87
89
  | `DEEPGRAM_TTS_VOICE` | `aura-2-asteria-en` | Deepgram TTS voice name |
88
90
  | `DEEPGRAM_TTS_SPEED` | `1.3` | Deepgram TTS playback speed (range 0.5–1.5) |
89
91
  | `CARTESIA_API_KEY` | — | Required for cartesia TTS |
92
+ | `ANTHROPIC_API_KEY` | — | Enables the Anthropic REST adapter; when set and voice is enabled, voice auto-wires to the direct Messages API path (zero CLI cold-start). See `runtime.md § Anthropic REST Runtime`. |
90
93
  | *(built-in)* | — | Telegraphic style instruction hardcoded into every voice AI invocation — front-loads the answer, strips preambles/markdown/filler, keeps responses short for TTS latency. Not an env var; not overridable by `DISCOCLAW_VOICE_SYSTEM_PROMPT`. |
package/.env.example CHANGED
@@ -59,9 +59,24 @@ DISCORD_GUILD_ID=
59
59
  #CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX=1
60
60
  # Optional: isolate Codex state/sessions from ~/.codex (helps avoid stale rollout DB issues):
61
61
  #CODEX_HOME=/absolute/path/to/.codex-home-discoclaw
62
+ # Runtime launcher state hardening for CLI providers.
63
+ # When enabled, launcher state/path errors (e.g. Codex rollout-path corruption) trigger
64
+ # one automatic retry with CODEX_HOME set to a clean stable home.
65
+ # Set to 0 to disable this behavior.
66
+ #DISCOCLAW_CLI_LAUNCHER_STATE_HARDENING=1
67
+ # Optional stable home override used by the hardening retry above.
68
+ # Default: <discoclaw cwd>/.codex-home-discoclaw
69
+ #DISCOCLAW_CODEX_STABLE_HOME=/absolute/path/to/.codex-home-discoclaw
62
70
  # Disable Codex session persistence/resume (workaround for session DB issues):
63
71
  #CODEX_DISABLE_SESSIONS=1
64
-
72
+ # Emit Codex item lifecycle debug events in stream preview (item.started/item.completed + item.type):
73
+ #DISCOCLAW_CODEX_ITEM_TYPE_DEBUG=1
74
+ # Log each Discord preview line decision (allowed/suppressed + rendered line) to journald:
75
+ #DISCOCLAW_DEBUG_STREAM_PREVIEW_LINES=1
76
+
77
+ # [DEPRECATED] Model configuration has moved to models.json (managed via !models commands).
78
+ # RUNTIME_MODEL is still read as a fallback when models.json is missing, but new deployments
79
+ # should use `!models set chat <model>` instead. See docs for migration details.
65
80
  # Model tier: fast | capable | deep (provider-agnostic).
66
81
  # Concrete model names (e.g. opus, sonnet, gpt-4o) are still accepted as passthrough.
67
82
  #RUNTIME_MODEL=capable
@@ -117,6 +132,7 @@ DISCORD_GUILD_ID=
117
132
  # Only set this to override the auto-discovered channel.
118
133
  #DISCOCLAW_VOICE_LOG_CHANNEL=
119
134
  #DEEPGRAM_API_KEY=
135
+ #ANTHROPIC_API_KEY=
120
136
 
121
137
  # ----------------------------------------------------------
122
138
  # Secret management via Discord DM
package/.env.example.full CHANGED
@@ -49,14 +49,26 @@ DISCORD_ALLOW_USER_IDS=
49
49
  #PRIMARY_RUNTIME=claude
50
50
 
51
51
  # --- Primary models ---
52
+ # [DEPRECATED] Model configuration has moved to models.json (managed via !models commands).
53
+ # RUNTIME_MODEL is still read as a fallback when models.json is missing, but new deployments
54
+ # should use `!models set chat <model>` instead. See docs for migration details.
52
55
  # Model tier: fast | capable | deep (provider-agnostic).
53
56
  # Concrete model names (e.g. opus, sonnet, gpt-4o) are still accepted as passthrough.
54
57
  #RUNTIME_MODEL=capable
55
58
 
56
59
  # --- Fast-tier default ---
60
+ # [DEPRECATED] Model configuration has moved to models.json (managed via !models commands).
61
+ # DISCOCLAW_FAST_MODEL is still read as a fallback when models.json is missing, but new
62
+ # deployments should use `!models set fast <model>` instead.
57
63
  # Sets the default model for all "small" tasks (summary, cron, cron auto-tag, task auto-tag).
58
64
  # Valid tiers: fast | capable | deep. Individual overrides (DISCOCLAW_SUMMARY_MODEL, etc.) still win when set.
59
65
  #DISCOCLAW_FAST_MODEL=fast
66
+ # [DEPRECATED] Fast-tier runtime selection has moved to models.json (managed via !models commands).
67
+ # Use `!models set fast <provider>/<model>` to route fast-tier through a different runtime —
68
+ # the provider prefix auto-selects the runtime adapter. DISCOCLAW_FAST_RUNTIME is still read
69
+ # as a fallback when models.json has no fast-tier entry, but new deployments should migrate.
70
+ # Valid values: claude | gemini | codex | openai | openrouter
71
+ #DISCOCLAW_FAST_RUNTIME=
60
72
 
61
73
  # --- Tier model overrides ---
62
74
  # Override the concrete model resolved for any runtime × tier combination.
@@ -177,6 +189,10 @@ DISCORD_GUILD_ID=
177
189
  # Per-category flags (only active when master switch is 1):
178
190
  #DISCOCLAW_DISCORD_ACTIONS_CHANNELS=1
179
191
  #DISCOCLAW_DISCORD_ACTIONS_MESSAGING=1
192
+ # Comma-separated absolute directory paths allowed for the sendFile Discord action.
193
+ # Defaults to /tmp when unset. DISCOCLAW_DATA_DIR and WORKSPACE_CWD are always
194
+ # auto-included when configured. Symlinks are resolved via fs.realpath() before checking.
195
+ #DISCOCLAW_SENDFILE_ALLOWED_DIRS=/tmp
180
196
  #DISCOCLAW_DISCORD_ACTIONS_GUILD=1
181
197
  # Intentionally off — moderation actions require explicit opt-in.
182
198
  #DISCOCLAW_DISCORD_ACTIONS_MODERATION=0
@@ -255,6 +271,10 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
255
271
  #DISCOCLAW_SUMMARY_MODEL=fast
256
272
  #DISCOCLAW_SUMMARY_MAX_CHARS=2000
257
273
  #DISCOCLAW_SUMMARY_EVERY_N_TURNS=5
274
+ # Estimated token threshold for one-pass summary recompression.
275
+ #DISCOCLAW_SUMMARY_MAX_TOKENS=1500
276
+ # Compression target ratio used when recompression runs (target = max_tokens * ratio).
277
+ #DISCOCLAW_SUMMARY_TARGET_RATIO=0.65
258
278
  # Override storage directory for rolling summaries.
259
279
  #DISCOCLAW_SUMMARY_DATA_DIR=
260
280
  # Durable per-user facts/preferences (manual via !memory commands).
@@ -287,6 +307,34 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
287
307
  # Character budget for recent conversation history in prompts (0 = disabled).
288
308
  #DISCOCLAW_MESSAGE_HISTORY_BUDGET=3000
289
309
 
310
+ # ----------------------------------------------------------
311
+ # Cold storage — vector-indexed conversation history (off by default)
312
+ # ----------------------------------------------------------
313
+ # Master switch — enables SQLite + sqlite-vec backed long-term memory.
314
+ # When enabled, messages are chunked, embedded, and stored for semantic
315
+ # retrieval. Matching context is injected into prompts automatically.
316
+ #DISCOCLAW_COLD_STORAGE_ENABLED=0
317
+ # API key for the embedding provider. Falls back to OPENAI_API_KEY when unset.
318
+ # For the default "openai" provider, this is your OpenAI API key.
319
+ #COLD_STORAGE_API_KEY=
320
+ # Embedding provider: openai (default) or openai-compat (any OpenAI-compatible API).
321
+ #COLD_STORAGE_PROVIDER=openai
322
+ # Model name for embeddings (required for openai-compat; optional for openai).
323
+ #COLD_STORAGE_MODEL=
324
+ # Embedding dimensions (required for openai-compat; optional for openai).
325
+ #COLD_STORAGE_DIMENSIONS=
326
+ # Base URL for the embedding API (required for openai-compat; optional for openai).
327
+ #COLD_STORAGE_BASE_URL=
328
+ # Path to the SQLite database file. Default: <data-dir>/cold-storage.db
329
+ #COLD_STORAGE_DB_PATH=
330
+ # Max characters for the cold-storage prompt section injected into each prompt (default: 1500).
331
+ #DISCOCLAW_COLD_STORAGE_INJECT_MAX_CHARS=1500
332
+ # Max search results returned per query (default: 10).
333
+ #DISCOCLAW_COLD_STORAGE_SEARCH_LIMIT=10
334
+ # Comma-separated channel IDs to restrict cold-storage ingestion and retrieval.
335
+ # When set, only messages from these channels are stored/searched. Empty = all channels.
336
+ #COLD_STORAGE_CHANNEL_FILTER=
337
+
290
338
  # ----------------------------------------------------------
291
339
  # Bot identity
292
340
  # ----------------------------------------------------------
@@ -365,6 +413,21 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
365
413
  #DISCOCLAW_ACTION_FOLLOWUP_DEPTH=3
366
414
  # Timeout for runtime invocations (ms).
367
415
  #RUNTIME_TIMEOUT_MS=1800000
416
+ # Global runtime supervisor wrapper (off by default; preserves legacy behavior).
417
+ # When enabled, all runtime invocations run through plan -> execute -> evaluate -> decide.
418
+ #DISCOCLAW_GLOBAL_SUPERVISOR_ENABLED=0
419
+ # Emit supervisor cycle audit JSON on stdout or stderr.
420
+ #DISCOCLAW_GLOBAL_SUPERVISOR_AUDIT_STREAM=stderr
421
+ # Max supervisor cycles before forced bail (must be >= 1).
422
+ #DISCOCLAW_GLOBAL_SUPERVISOR_MAX_CYCLES=3
423
+ # Max retries allowed across cycles (must be >= 0).
424
+ #DISCOCLAW_GLOBAL_SUPERVISOR_MAX_RETRIES=2
425
+ # Max escalation prompt level applied across retries (must be >= 0).
426
+ #DISCOCLAW_GLOBAL_SUPERVISOR_MAX_ESCALATION_LEVEL=2
427
+ # Hard cap on total streamed events across all cycles (must be >= 1).
428
+ #DISCOCLAW_GLOBAL_SUPERVISOR_MAX_TOTAL_EVENTS=5000
429
+ # Wall-time cap for the full supervisor loop (ms). 0 disables wall-time cap.
430
+ #DISCOCLAW_GLOBAL_SUPERVISOR_MAX_WALL_TIME_MS=0
368
431
  # Multi-turn mode: persistent subprocess per session, keeping session context across messages (default: 1).
369
432
  #DISCOCLAW_MULTI_TURN=1
370
433
  # Timeout (ms) before a multi-turn process is considered hung and restarted.
@@ -377,18 +440,26 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
377
440
  #DISCOCLAW_SESSION_SCANNING=1
378
441
  # Parse tool-use events during streaming for better progress reporting and stall suppression.
379
442
  #DISCOCLAW_TOOL_AWARE_STREAMING=1
443
+ # Render a denser "Thinking..." streaming preview tail (more lines + wider logs + richer tool signals).
444
+ # Helps previews update more visibly during long runs; action tags are still stripped from preview text.
445
+ #DISCOCLAW_STREAM_PREVIEW_RAW=0
380
446
  # Stream stall detection: kill one-shot process if no stdout/stderr for this long (ms). 0 = disabled. (default: 600000)
381
447
  #DISCOCLAW_STREAM_STALL_TIMEOUT_MS=120000
382
448
  # Progress stall timeout: alert after this many ms with no progress event (ms). 0 = disabled.
383
449
  #DISCOCLAW_PROGRESS_STALL_TIMEOUT_MS=300000
384
450
  # Stream stall warning: show user-visible warning in Discord after this many ms of no events. 0 = disabled. (default: 300000)
385
451
  #DISCOCLAW_STREAM_STALL_WARNING_MS=60000
386
- # Post a brief "Done (Xm Ys)" completion notice as a new message after long-running runs finish.
387
- # Discord's unread indicator fires on new messages but not edits, so users who left the channel
388
- # while the bot was working will see an unread badge. Set to 0 to disable.
452
+ # Enable long-run watchdog follow-up status updates.
453
+ # Normal path: after the threshold delay, a deferred "Still running..." check-in is posted.
454
+ # Recovery path: lifecycle state is persisted and swept on startup so interrupted long-running
455
+ # runs still get a final status update after restart.
456
+ # Persistence-first lifecycle: run completion is persisted before attempting the final Discord
457
+ # post/edit, and finalPosted is set only after a successful post/edit. Crash boundaries may
458
+ # duplicate follow-up/final updates, but should not omit them. Set to 0 to disable watchdog
459
+ # follow-up posting entirely.
389
460
  #DISCOCLAW_COMPLETION_NOTIFY=1
390
- # Minimum elapsed time (ms) before a completion notice is sent. Runs shorter than this are
391
- # considered "fast" and don't need a notification. Default: 30000 (30 seconds).
461
+ # Delay (ms) before the in-process "Still running..." follow-up timer fires. Fast runs shorter
462
+ # than this are considered non-long-running and do not post watchdog follow-ups. Default: 30000.
392
463
  #DISCOCLAW_COMPLETION_NOTIFY_THRESHOLD_MS=30000
393
464
 
394
465
  # ----------------------------------------------------------
@@ -405,6 +476,10 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
405
476
  #PLAN_PHASE_TIMEOUT_MS=1800000
406
477
  # Max audit-fix attempts per phase before marking failed.
407
478
  #PLAN_PHASE_AUDIT_FIX_MAX=3
479
+ # Default heartbeat interval (ms) for command-path phase progress updates
480
+ # in `!plan run*` and `!forge`. Set to 0 to disable periodic heartbeats;
481
+ # phase starts/transitions and the single terminal outcome still post.
482
+ #PLAN_FORGE_HEARTBEAT_INTERVAL_MS=45000
408
483
  # Max draft-audit-revise loops before CAP_REACHED.
409
484
  #FORGE_MAX_AUDIT_ROUNDS=5
410
485
  # Model overrides for forge roles (fall back to RUNTIME_MODEL).
@@ -434,7 +509,7 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
434
509
  # Optional: isolate Codex state/sessions from ~/.codex (helps avoid stale rollout DB issues).
435
510
  #CODEX_HOME=/absolute/path/to/.codex-home-discoclaw
436
511
  # Default model for the Codex CLI adapter. Used when FORGE_AUDITOR_MODEL is not set.
437
- #CODEX_MODEL=gpt-5.3-codex
512
+ #CODEX_MODEL=gpt-5.4
438
513
  # WARNING: disables Codex approval prompts and sandbox protections (full-access mode).
439
514
  # Equivalent to passing --dangerously-bypass-approvals-and-sandbox to codex exec.
440
515
  #CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX=0
@@ -540,13 +615,17 @@ DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=0
540
615
  # Leave unset to disable transcript mirroring.
541
616
  #DISCOCLAW_VOICE_LOG_CHANNEL= # e.g. "voice-log" if using the default scaffold
542
617
  # Model for voice AI responses: tier (fast | capable | deep) or concrete name (sonnet, opus, haiku).
543
- # Independent of RUNTIME_MODEL — allows tuning voice latency vs quality separately from chat.
544
- # Switchable at runtime via `modelSet voice <model>`.
545
- # Default: follows DISCOCLAW_FAST_MODEL (override here for voice-specific tuning).
618
+ # Independent of the chat model — allows tuning voice latency vs quality separately from chat.
619
+ # Switchable at runtime via `!models set voice <model>`.
620
+ # Default: follows the startup chat model unless overridden here.
546
621
  #DISCOCLAW_VOICE_MODEL=sonnet
547
622
  # Custom system prompt prepended to voice AI invocations. Max 4000 chars.
548
623
  # Use this to set a conversational tone, brevity instructions, or persona for voice responses.
549
624
  #DISCOCLAW_VOICE_SYSTEM_PROMPT=
625
+ # Anthropic API key for direct Messages API access (bypasses Claude CLI cold-start).
626
+ # When set and voice is enabled, voice invocations use the Anthropic REST adapter
627
+ # instead of the CLI subprocess, eliminating ~2-5s cold-start latency per response.
628
+ #ANTHROPIC_API_KEY=
550
629
  # API key for Deepgram Nova-3 STT. Required when DISCOCLAW_STT_PROVIDER=deepgram.
551
630
  #DEEPGRAM_API_KEY=
552
631
  # Deepgram STT model for voice transcription (default: nova-3-conversationalai).
package/README.md CHANGED
@@ -26,6 +26,7 @@ Your assistant carries context across every conversation, channel, and restart.
26
26
 
27
27
  - **Durable facts** — `!memory remember prefers dark mode` persists across sessions and channels
28
28
  - **Rolling summaries** — Compresses earlier conversation so context carries forward, even across restarts
29
+ - **Cold storage** — Semantic search over past conversations using vector embeddings + keyword search. Relevant history is automatically retrieved and injected into the prompt (see [docs/memory.md](docs/memory.md))
29
30
  - **Per-channel context** — Each channel gets a markdown file shaping behavior (formal in #work, casual in #random)
30
31
  - **Customizable identity** — Personality, name, and values defined in workspace files (`SOUL.md`, `IDENTITY.md`, etc.)
31
32
  - **Group chat aware** — Knows when to speak up and when to stay quiet in shared channels
@@ -84,6 +85,24 @@ DiscoClaw orchestrates the flow between Discord and AI runtimes (Claude Code by
84
85
  4. Streams the response back, chunked to fit Discord's message limits
85
86
  5. Parses and executes any Discord actions the assistant emitted
86
87
 
88
+ ### Instruction precedence
89
+
90
+ Prompt assembly has two layers, each with its own ordering contract.
91
+
92
+ **Preamble precedence** — the front of every prompt, in strict priority order:
93
+
94
+ 1. **Immutable security policy** (hard-coded root rules)
95
+ 2. **Tracked defaults** (runtime-injected from `templates/instructions/SYSTEM_DEFAULTS.md`)
96
+ 3. **Tracked tools** (runtime-injected from `templates/instructions/TOOLS.md`)
97
+ 4. **User rules override** (`workspace/AGENTS.md`)
98
+ 5. **User tools override** (`workspace/TOOLS.md`, optional)
99
+ 6. **Memory/context layers** (workspace identity files, channel context, durable/rolling memory, etc.)
100
+
101
+ **Post-preamble section ordering** — the sections between the preamble and the user message are arranged to exploit primacy bias (high-signal sections first) and recency bias (action schemas and constraints near the end, just before the user message). Low-signal data sections sit in the middle. See [`docs/prompt-ordering.md`](docs/prompt-ordering.md) for the canonical order and rationale.
102
+
103
+ `workspace/DISCOCLAW.md` is no longer a managed or authoritative instruction source.
104
+ If you still have a legacy copy, treat it as historical reference only.
105
+
87
106
  ### Message batching
88
107
 
89
108
  When multiple messages arrive while the bot is thinking (i.e., an AI invocation is already active for that session), they're automatically combined into a single prompt rather than queued individually. This means rapid follow-up messages are processed together, giving the bot full context in one shot. Commands (`!`-prefixed messages) bypass batching and are always processed individually.
@@ -96,7 +115,7 @@ Required: `OPENROUTER_API_KEY`. Optional overrides: `OPENROUTER_BASE_URL` (defau
96
115
 
97
116
  ## Model Overrides
98
117
 
99
- The `!models` command lets you view and swap AI models per role at runtime — no restart needed, and changes persist across restarts.
118
+ The `!models` command lets you view and swap AI models per role at runtime — no restart needed. Changes are persisted to `models.json` under the data dir and survive restarts.
100
119
 
101
120
  **Roles:** `chat`, `fast`, `forge-drafter`, `forge-auditor`, `summary`, `cron`, `cron-exec`, `voice`
102
121
 
@@ -104,17 +123,18 @@ The `!models` command lets you view and swap AI models per role at runtime — n
104
123
  |---------|-------------|
105
124
  | `!models` | Show current model assignments |
106
125
  | `!models set <role> <model>` | Change the model for a role |
107
- | `!models reset` | Revert all roles to env-var defaults |
108
- | `!models reset <role>` | Revert a specific role |
126
+ | `!models reset` | Revert all roles to startup defaults and clear overrides |
127
+ | `!models reset <role>` | Revert a specific role to its startup default |
109
128
 
110
129
  **Examples:**
111
130
  - `!models set chat claude-sonnet-4` — use Sonnet for chat
112
131
  - `!models set chat openrouter` — switch chat to the OpenRouter runtime
113
132
  - `!models set cron-exec haiku` — run crons on a cheaper model
133
+ - `!models set cron-exec default` — clear the cron-exec override and use the startup default again
114
134
  - `!models set voice sonnet` — use a specific model for voice
115
- - `!models reset` — clear all overrides
135
+ - `!models reset` — clear all overrides and revert to startup defaults
116
136
 
117
- Setting the `chat` role to a runtime name (`openrouter`, `openai`, `gemini`, `codex`, `claude`) switches the active runtime adapter for that role.
137
+ Setting the `chat` or `voice` role to a runtime name (`openrouter`, `openai`, `gemini`, `codex`, `claude`) switches the active runtime adapter for that role.
118
138
 
119
139
  ## Secret Management
120
140
 
@@ -165,6 +185,15 @@ When using the Claude runtime, you can connect external tool servers via MCP. Pl
165
185
  **Contributors (from source):**
166
186
  - Everything above, plus **pnpm** — enable via Corepack (`corepack enable`) or install separately
167
187
 
188
+ ### Model capability requirement
189
+
190
+ DiscoClaw assumes reliable structured output for several runtime paths (for example: Discord actions, cron JSON routing, and tool-call loops).
191
+
192
+ - For OpenAI-compatible and OpenRouter adapters, pick models that reliably support JSON-shaped output and function calling.
193
+ - "OpenAI-compatible" API shape alone is not a capability guarantee.
194
+ - If a model fails JSON/tool-call smoke tests, treat it as unsupported for DiscoClaw runtime use.
195
+ - Use the [model validation smoke test checklist](docs/configuration.md#model-validation-smoke-test-recommended) before adopting a new model.
196
+
168
197
  <!-- source-of-truth: docs/discord-bot-setup.md -->
169
198
  ## Quick start
170
199
 
@@ -221,17 +250,12 @@ Full step-by-step guide: [docs/discord-bot-setup.md](docs/discord-bot-setup.md)
221
250
  npm install -g discoclaw
222
251
  ```
223
252
 
224
- > **Fedora 43+ / GCC 14+ — `@discordjs/opus` build failure**
253
+ > **Fedora 43+ / GCC 14+ — `@discordjs/opus` build failure (resolved)**
225
254
  >
226
- > GCC 14 promotes `-Wincompatible-pointer-types` to a hard error by default. The upstream opus C source triggers this, causing `npm install` to fail with an error like:
227
- > ```
228
- > error: incompatible pointer types passing ...
229
- > ```
230
- > **Workaround** — set the flag before installing:
255
+ > This was fixed upstream in `@discordjs/opus` 0.10.0. If you are pinned to an older version, set the flag before installing:
231
256
  > ```bash
232
257
  > CFLAGS="-Wno-error=incompatible-pointer-types" npm install -g discoclaw
233
258
  > ```
234
- > This is a known upstream issue in the `@discordjs/opus` native addon. It only requires the flag override at install time; runtime behavior is unaffected.
235
259
 
236
260
  2. **Run the interactive setup wizard** (creates `.env` and scaffolds your workspace):
237
261
  ```bash
@@ -44,6 +44,8 @@ export function buildEnvContent(vals, now = new Date()) {
44
44
  'GEMINI_BIN',
45
45
  'GEMINI_MODEL',
46
46
  'OPENAI_API_KEY',
47
+ 'DISCOCLAW_FAST_RUNTIME',
48
+ 'DISCOCLAW_TIER_OPENAI_FAST',
47
49
  'CODEX_BIN',
48
50
  'CODEX_MODEL',
49
51
  'CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX',
@@ -158,6 +160,19 @@ export async function runInitWizard() {
158
160
  console.log(` Error: ${err}. Try again.\n`);
159
161
  }
160
162
  }
163
+ async function askOptional(prompt, validate) {
164
+ while (true) {
165
+ if (canceled)
166
+ return '';
167
+ const val = await ask(prompt);
168
+ if (!val.trim())
169
+ return '';
170
+ const err = validate(val.trim());
171
+ if (!err)
172
+ return val.trim();
173
+ console.log(` Error: ${err}. Try again.\n`);
174
+ }
175
+ }
161
176
  // ── Welcome ──────────────────────────────────────────────────────────────
162
177
  console.log(`\nDiscoclaw Init\n==============`);
163
178
  const installDirInput = await ask(`Install directory [${cwd}]: `);
@@ -307,6 +322,13 @@ export async function runInitWizard() {
307
322
  }
308
323
  else if (finalChoice === '4') {
309
324
  values.PRIMARY_RUNTIME = 'codex';
325
+ const openaiFastKey = await askOptional('Optional OpenAI API key for fast tier (gpt-5-mini) [leave empty to skip]: ', () => null);
326
+ if (openaiFastKey) {
327
+ values.OPENAI_API_KEY = openaiFastKey;
328
+ values.DISCOCLAW_FAST_RUNTIME = 'openai';
329
+ values.DISCOCLAW_TIER_OPENAI_FAST = 'gpt-5-mini';
330
+ console.log(' Fast-tier split enabled: chat=codex, fast=openai (gpt-5-mini).');
331
+ }
310
332
  }
311
333
  else if (finalChoice === '5') {
312
334
  values.PRIMARY_RUNTIME = 'openrouter';
@@ -106,6 +106,20 @@ describe('init wizard helpers', () => {
106
106
  expect(content).toContain('DISCOCLAW_TASKS_FORUM=1000000000000000002');
107
107
  expect(content).toContain('DISCOCLAW_CRON_FORUM=1000000000000000003');
108
108
  });
109
+ it('includes codex fast-runtime split keys in generated env content', () => {
110
+ const content = buildEnvContent({
111
+ DISCORD_TOKEN: 'a.b.c',
112
+ DISCORD_ALLOW_USER_IDS: '1000000000000000001',
113
+ PRIMARY_RUNTIME: 'codex',
114
+ OPENAI_API_KEY: 'sk-fast-key',
115
+ DISCOCLAW_FAST_RUNTIME: 'openai',
116
+ DISCOCLAW_TIER_OPENAI_FAST: 'gpt-5-mini',
117
+ }, new Date('2026-03-04T00:00:00.000Z'));
118
+ expect(content).toContain('PRIMARY_RUNTIME=codex');
119
+ expect(content).toContain('OPENAI_API_KEY=sk-fast-key');
120
+ expect(content).toContain('DISCOCLAW_FAST_RUNTIME=openai');
121
+ expect(content).toContain('DISCOCLAW_TIER_OPENAI_FAST=gpt-5-mini');
122
+ });
109
123
  it('writes DISCOCLAW_DATA_DIR in required section when provided', () => {
110
124
  const content = buildEnvContent({
111
125
  DISCORD_TOKEN: 'a.b.c',
@@ -259,6 +273,39 @@ describe('runInitWizard', () => {
259
273
  expect(newEnv).toContain('DISCOCLAW_DISCORD_ACTIONS=1');
260
274
  expect(newEnv).toContain(`DISCOCLAW_DATA_DIR=${path.join(tmpDir, 'data')}`);
261
275
  });
276
+ it('writes codex fast-runtime split config when provider 4 and OpenAI key are provided', async () => {
277
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
278
+ const previousCwd = process.cwd();
279
+ const answers = [
280
+ '', // install directory (default)
281
+ '', // Press Enter to continue
282
+ '', // data directory (default cwd/data)
283
+ 'a.b.c', // DISCORD_TOKEN
284
+ '1000000000000000001', // DISCORD_ALLOW_USER_IDS
285
+ '5000000000000000001', // DISCORD_GUILD_ID
286
+ '4', // provider selection -> Codex
287
+ 'sk-fast-key', // optional fast OpenAI API key
288
+ 'n', // enable voice -> no
289
+ ];
290
+ process.chdir(tmpDir);
291
+ vi.mocked(createInterface).mockReturnValue(makeReadline(answers));
292
+ vi.mocked(execFileSync).mockImplementation(() => {
293
+ throw new Error('binary not found');
294
+ });
295
+ vi.mocked(ensureWorkspaceBootstrapFiles).mockResolvedValue([]);
296
+ vi.spyOn(console, 'log').mockImplementation(() => { });
297
+ try {
298
+ await runInitWizard();
299
+ }
300
+ finally {
301
+ process.chdir(previousCwd);
302
+ }
303
+ const newEnv = fs.readFileSync(path.join(tmpDir, '.env'), 'utf8');
304
+ expect(newEnv).toContain('PRIMARY_RUNTIME=codex');
305
+ expect(newEnv).toContain('OPENAI_API_KEY=sk-fast-key');
306
+ expect(newEnv).toContain('DISCOCLAW_FAST_RUNTIME=openai');
307
+ expect(newEnv).toContain('DISCOCLAW_TIER_OPENAI_FAST=gpt-5-mini');
308
+ });
262
309
  it('always writes DISCOCLAW_DATA_DIR when a custom path is given', async () => {
263
310
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
264
311
  const previousCwd = process.cwd();