discoclaw 1.2.1 → 1.2.3
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/.context/discord.md +14 -0
- package/.context/pa.md +6 -0
- package/.env.example +9 -0
- package/.env.example.full +5 -0
- package/README.md +38 -475
- package/dist/config.js +1 -0
- package/dist/discord/action-categories.js +18 -2
- package/dist/discord/capsule-invalidation.js +52 -0
- package/dist/discord/capsule-invalidation.test.js +108 -0
- package/dist/discord/message-coordinator.js +8 -0
- package/dist/discord/message-coordinator.test.js +1 -1
- package/dist/discord/output-common.js +12 -2
- package/dist/discord/output-common.test.js +38 -2
- package/dist/index.js +2 -0
- package/package.json +16 -1
package/.context/discord.md
CHANGED
|
@@ -96,6 +96,8 @@ Notes:
|
|
|
96
96
|
- `generateImage` supports two providers: **OpenAI** (models: `dall-e-3`, `gpt-image-1`) and **Gemini** (models: `imagen-4.0-generate-001`, `imagen-4.0-fast-generate-001`, `imagen-4.0-ultra-generate-001`). Provider is auto-detected from the model prefix (`dall-e-*`/`gpt-image-*` → openai, `imagen-*` → gemini) or set explicitly via the `provider` field. OpenAI provider uses `OPENAI_API_KEY` (required) and optional `OPENAI_BASE_URL`. Gemini provider uses `IMAGEGEN_GEMINI_API_KEY`. At least one key must be set when `DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=1`. Default model is auto-detected: if only `IMAGEGEN_GEMINI_API_KEY` is set, defaults to `imagen-4.0-generate-001`; otherwise defaults to `dall-e-3`. Override with `IMAGEGEN_DEFAULT_MODEL`.
|
|
97
97
|
- `spawnAgent` is enabled by default (`DISCOCLAW_DISCORD_ACTIONS_SPAWN=1`; set to 0 to disable). Spawned agents run fire-and-forget: each agent runs its prompt via the configured runtime and posts its output directly to the target channel. Multiple `spawnAgent` actions in a single response run in parallel (bounded by `DISCOCLAW_DISCORD_ACTIONS_SPAWN_MAX_CONCURRENT`, default 8). Spawn is disabled for bot-originated messages and excluded from cron flows to prevent recursive agent chains. Spawned agents run at recursion depth 1 and cannot themselves spawn further agents.
|
|
98
98
|
|
|
99
|
+
Action guard (false completion detection): When a reply's visible text claims Discord-managed work was performed or is being performed — in any tense (present progressive, future intent, past tense, or perfect tense) — but the turn produced zero actionable `<discord-action>` blocks and zero executed action results, the finalizer appends a visible warning. This catches fabricated completion claims ("Posted the plan", "I've sent the message") at the output boundary so the user sees the discrepancy immediately. The guard is implemented in `output-common.ts` (`claimsImmediateDiscordActionIntent` + `appendPromisedDiscordActionWithoutExecutionNotice`) and requires no prompt-layer changes — it operates purely on the finalized reply text and action execution counts.
|
|
100
|
+
|
|
99
101
|
Auto-follow-up: When query actions (channelList, channelInfo, threadListArchived, forumTagList, readMessages, fetchMessage, listPins, memberInfo, roleInfo, searchMessages, eventList, taskList, taskShow, cronList, cronShow, planList, planShow, memoryShow, modelShow, forgeStatus) succeed, DiscoClaw automatically re-invokes Claude with the results. This allows Claude to reason about query results without requiring the user to send a follow-up message. Controlled by `DISCOCLAW_ACTION_FOLLOWUP_DEPTH` (default `3`, `0` disables). Mutation-only responses do not trigger follow-ups. Trivially short follow-up responses (<50 chars with no actions) are suppressed.
|
|
100
102
|
|
|
101
103
|
Requirements:
|
|
@@ -236,6 +238,18 @@ grep DISCOCLAW_STATUS_CHANNEL .env
|
|
|
236
238
|
journalctl --user -u discoclaw.service --since "5 min ago" --no-pager | grep -i "status"
|
|
237
239
|
```
|
|
238
240
|
|
|
241
|
+
### Bot claims it performed a Discord action but nothing happened
|
|
242
|
+
**Symptom:** Bot says "Posted the plan to #general" or "I've sent the message" but no message appears in the target channel.
|
|
243
|
+
**Cause:** The model fabricated a completion claim without emitting a `<discord-action>` block. The action guard should append a visible warning to the reply.
|
|
244
|
+
**Verification:**
|
|
245
|
+
1. Check whether the reply ends with a warning starting with `Warning: this reply says Discord-managed work was performed`.
|
|
246
|
+
2. If the warning is present, the guard is working — the model hallucinated the action. No code fix needed; the warning tells the user.
|
|
247
|
+
3. If no warning is present but the action still didn't execute, check whether the action block was parsed but failed during execution (look for "Action Failed" in the status channel or bot logs).
|
|
248
|
+
```bash
|
|
249
|
+
# Check recent action failures
|
|
250
|
+
journalctl --user -u discoclaw.service --since "5 min ago" --no-pager | grep -i "action.*fail\|warning.*discord-action"
|
|
251
|
+
```
|
|
252
|
+
|
|
239
253
|
### Messages split awkwardly across Discord's 2000-char limit
|
|
240
254
|
**Symptom:** Bot replies are split mid-sentence or mid-code-block, producing garbled formatting.
|
|
241
255
|
**Cause:** The chunking algorithm tries to preserve code fences but can't always split cleanly if the response has deeply nested or very long code blocks.
|
package/.context/pa.md
CHANGED
|
@@ -40,6 +40,12 @@ Your prompt may include:
|
|
|
40
40
|
- **Durable memory** — Persistent user facts/preferences. Treat as ground truth unless contradicted.
|
|
41
41
|
- **Conversation memory** — Rolling summary, lossy. Trust recent messages over summary if they conflict.
|
|
42
42
|
|
|
43
|
+
## False Completion Claims
|
|
44
|
+
|
|
45
|
+
The runtime action guard (`output-common.ts`) catches both present-tense intent ("I'm posting…") and past-tense completion claims ("Posted the plan to the channel", "I've sent the message") that lack matching `<discord-action>` blocks. When the guard fires, it appends a visible warning to the reply so the user sees the discrepancy immediately.
|
|
46
|
+
|
|
47
|
+
No separate prompt or instruction change is needed to address this gap — the guard operates at the output finalization boundary and catches false claims regardless of how the model phrases them. If the model fabricates a completion claim, the warning surfaces it before the user has to ask why nothing happened.
|
|
48
|
+
|
|
43
49
|
## Autonomy Tiers
|
|
44
50
|
|
|
45
51
|
**Always OK:** Read files, explore, search web, run diagnostics, send Discord messages, react, share finds, work within workspace.
|
package/.env.example
CHANGED
|
@@ -234,6 +234,15 @@ DISCORD_GUILD_ID=
|
|
|
234
234
|
# "anthropic-cache-telemetry" in journal output). Set LOG_LEVEL=debug for
|
|
235
235
|
# full prompt section breakdowns.
|
|
236
236
|
|
|
237
|
+
# ----------------------------------------------------------
|
|
238
|
+
# Continuation capsule staleness
|
|
239
|
+
# ----------------------------------------------------------
|
|
240
|
+
# Maximum age (ms) of a continuation capsule before it is skipped at injection time.
|
|
241
|
+
# Prevents stale context from prior conversations bleeding into unrelated sessions.
|
|
242
|
+
# Capsules with an idle/awaiting currentFocus are always skipped regardless of age.
|
|
243
|
+
# Default: 7200000 (2 hours). Set to 0 to disable TTL expiry (idle detection still applies).
|
|
244
|
+
#DISCOCLAW_CAPSULE_TTL_MS=7200000
|
|
245
|
+
|
|
237
246
|
# ----------------------------------------------------------
|
|
238
247
|
# For all ~90 options (subsystems, actions, memory, identity,
|
|
239
248
|
# observability, advanced/debug), see .env.example.full
|
package/.env.example.full
CHANGED
|
@@ -369,6 +369,11 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
|
|
|
369
369
|
#DISCOCLAW_MEMORY_CONSOLIDATION_MODEL=fast
|
|
370
370
|
# Character budget for recent conversation history in prompts (0 = disabled).
|
|
371
371
|
#DISCOCLAW_MESSAGE_HISTORY_BUDGET=3000
|
|
372
|
+
# Maximum age (ms) of a continuation capsule before it is skipped at injection time.
|
|
373
|
+
# Prevents stale context from prior conversations bleeding into unrelated sessions.
|
|
374
|
+
# Capsules with an idle/awaiting/none currentFocus are always skipped regardless of age.
|
|
375
|
+
# Default: 7200000 (2 hours). Set to 0 to disable TTL expiry (idle detection still applies).
|
|
376
|
+
#DISCOCLAW_CAPSULE_TTL_MS=7200000
|
|
372
377
|
|
|
373
378
|
# ----------------------------------------------------------
|
|
374
379
|
# Cold storage — vector-indexed conversation history (off by default)
|
package/README.md
CHANGED
|
@@ -6,522 +6,85 @@
|
|
|
6
6
|
|
|
7
7
|
A personal AI orchestrator that turns Discord into a persistent workspace — built on three pillars: **Memory**, **Tasks**, and **Automations**.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/discoclaw)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](package.json)
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
> Turn Discord into a persistent AI workspace — memory, tasks, automations, and voice, all through natural conversation.
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
DiscoClaw coordinates between Discord, AI runtimes (Claude Code, OpenAI, Codex, Gemini, OpenRouter), and local system resources. The intelligence is rented; the coordination is owned. Designed for a single user on a private server — your own sandbox.
|
|
14
16
|
|
|
15
|
-
No gateways, no proxies, no web UI
|
|
16
|
-
|
|
17
|
-
The codebase is intentionally small — small enough to read, audit, and modify directly. Customization means changing the code, not configuring a plugin system.
|
|
18
|
-
|
|
19
|
-
## Why Discord?
|
|
20
|
-
|
|
21
|
-
Discord gives you channels, forum threads, DMs, mobile access, and rich formatting for free. DiscoClaw maps its three core features onto Discord primitives so there's nothing extra to learn — channels become context boundaries, forum threads become task cards and job definitions, and conversation history is the raw material for memory.
|
|
17
|
+
No gateways, no proxies, no web UI. Discord *is* the interface.
|
|
22
18
|
|
|
23
19
|
## Memory — the bot knows you
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **Per-channel context** — Each channel gets a markdown file shaping behavior (formal in #work, casual in #random)
|
|
31
|
-
- **Customizable identity** — Personality, name, and values defined in workspace files (`SOUL.md`, `IDENTITY.md`, etc.)
|
|
32
|
-
- **Group chat aware** — Knows when to speak up and when to stay quiet in shared channels
|
|
33
|
-
|
|
34
|
-
**Why Discord fits:** channels = context boundaries, DMs = private deep context, conversation history is the raw material.
|
|
35
|
-
|
|
36
|
-
### YouTube transcripts
|
|
37
|
-
|
|
38
|
-
When you share a YouTube link in a message, DiscoClaw automatically fetches the video's transcript and injects it into the AI's context. This lets the bot answer questions about video content, summarize talks, or reference specific points — without you needing to copy-paste anything. Up to 3 videos per message are processed, with a 15-second timeout per fetch. Transcripts are sanitized before injection to prevent prompt manipulation.
|
|
21
|
+
- **Durable facts** — persist across sessions, channels, and restarts
|
|
22
|
+
- **Rolling summaries** — context carries forward, even across restarts
|
|
23
|
+
- **Semantic search** — vector + keyword search over past conversations, auto-retrieved
|
|
24
|
+
- **Per-channel personality** — markdown files shape behavior per channel
|
|
25
|
+
- **YouTube transcripts** — share a link, the bot reads the video
|
|
39
26
|
|
|
40
27
|
## Tasks — the bot tracks your work
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
- **Status emoji and auto-tagging** — Thread names show live status at a glance
|
|
47
|
-
- **Discord actions** — Your assistant manages tasks through conversation: create channels, send messages, search history, run polls, and more (see [docs/discord-actions.md](docs/discord-actions.md))
|
|
48
|
-
|
|
49
|
-
**Why Discord fits:** forum threads = task cards, archive = done, thread names show live status.
|
|
29
|
+
- **Bidirectional sync** — task store and Discord forum threads stay in sync
|
|
30
|
+
- **Create from anywhere** — chat, commands, or the forum directly
|
|
31
|
+
- **Live status** — thread names show status emoji at a glance
|
|
32
|
+
- **Discord actions** — the bot manages channels, messages, polls, and more through conversation
|
|
50
33
|
|
|
51
34
|
## Automations — the bot acts on its own
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **
|
|
56
|
-
- **Edit to change, archive to pause, unarchive to resume**
|
|
57
|
-
- **Full workspace access** — File I/O, web search, browser automation, Discord actions
|
|
58
|
-
- **Multi-turn sessions** — A live process persists between runs, so context carries across executions
|
|
59
|
-
|
|
60
|
-
**Why Discord fits:** forum threads = job definitions, archive/unarchive = pause/resume, no separate scheduler UI needed.
|
|
61
|
-
|
|
62
|
-
For the managed Chrome/Chromium profile flow used by browser automation, see the [managed browser launcher guide](#managed-browser-launcher).
|
|
36
|
+
- **Plain-language schedules** — "every weekday at 7am, check the weather"
|
|
37
|
+
- **Forum-thread definitions** — edit to change, archive to pause
|
|
38
|
+
- **Full workspace access** — files, web, browser automation, Discord actions
|
|
63
39
|
|
|
64
|
-
<!-- source-of-truth: docs/voice.md -->
|
|
65
40
|
## Voice — the bot talks back
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- **STT** — Deepgram Nova-3 streaming transcription (WebSocket)
|
|
70
|
-
- **TTS** — Cartesia Sonic-3 speech synthesis (WebSocket, 24 kHz PCM)
|
|
71
|
-
- **Barge-in** — interrupt the bot mid-sentence by speaking; playback stops immediately
|
|
72
|
-
- **Auto-join** — optionally join/leave channels automatically when you enter or leave
|
|
73
|
-
- **Transcript mirror** — voice conversations are mirrored to a text channel for persistence
|
|
74
|
-
- **Voice actions** — the AI can execute a restricted action subset (messaging, tasks, memory) during voice
|
|
75
|
-
|
|
76
|
-
Voice is **off by default**. Enable with `DISCOCLAW_VOICE_ENABLED=1` plus API keys for your STT/TTS providers. Requires Node 22+ (for native WebSocket used by Cartesia TTS) and C++ build tools (for the `@discordjs/opus` native addon).
|
|
77
|
-
|
|
78
|
-
Full setup guide: [docs/voice.md](docs/voice.md)
|
|
79
|
-
|
|
80
|
-
## Self-management — the bot maintains itself
|
|
81
|
-
|
|
82
|
-
- **Self-update** — `!update` checks for new npm versions; `!update apply` downloads, installs, and restarts without leaving Discord
|
|
83
|
-
- **Health checks** — `!health`, `!doctor`, `!status` for diagnostics
|
|
84
|
-
- **Secret management** — `!secret` manages `.env` entries from DMs
|
|
85
|
-
- **Model switching** — `!models` swaps AI models per role at runtime
|
|
86
|
-
- **Restart** — `!restart` restarts the service on demand
|
|
87
|
-
|
|
88
|
-
## How it works
|
|
89
|
-
|
|
90
|
-
DiscoClaw orchestrates the flow between Discord and AI runtimes (Claude Code by default, with `gemini-api`, OpenAI, Codex, and OpenRouter adapters available via `PRIMARY_RUNTIME`). For 1.0, `Claude CLI` on a source checkout is the explicitly supported default path, and `Codex CLI` on a source checkout is the explicitly supported secondary path. See [docs/audit/provider-auth-1.0-matrix.md](docs/audit/provider-auth-1.0-matrix.md) for the full consolidated matrix. The OpenAI-compatible and OpenRouter adapters can expose optional tool use when `OPENAI_COMPAT_TOOLS_ENABLED=1` is set, but OpenRouter support claims stop at the narrower audited boundary described below. It doesn't contain intelligence itself — it decides *when* to call the AI, *what context* to give it, and *what to do* with the output. When you send a message, the orchestrator:
|
|
91
|
-
|
|
92
|
-
1. Checks the user allowlist (fail-closed — empty list means respond to nobody)
|
|
93
|
-
2. Assembles context: per-channel rules, conversation history, rolling summary, and durable memory
|
|
94
|
-
3. Routes to the appropriate runtime adapter, running in your workspace directory
|
|
95
|
-
4. Streams the response back, chunked to fit Discord's message limits
|
|
96
|
-
5. Parses and executes any Discord actions the assistant emitted
|
|
97
|
-
|
|
98
|
-
### Instruction precedence
|
|
99
|
-
|
|
100
|
-
Prompt assembly has two layers, each with its own ordering contract.
|
|
101
|
-
|
|
102
|
-
**Preamble precedence** — the front of every prompt, in strict priority order:
|
|
103
|
-
|
|
104
|
-
1. **Immutable security policy** (hard-coded root rules)
|
|
105
|
-
2. **Tracked defaults** (runtime-injected from `templates/instructions/SYSTEM_DEFAULTS.md`)
|
|
106
|
-
3. **Tracked tools** (runtime-injected from `templates/instructions/TOOLS.md`)
|
|
107
|
-
4. **User rules override** (`workspace/AGENTS.md`)
|
|
108
|
-
5. **User tools override** (`workspace/TOOLS.md`, optional)
|
|
109
|
-
6. **Memory/context layers** (workspace identity files, channel context, durable/rolling memory, etc.)
|
|
110
|
-
|
|
111
|
-
**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.
|
|
112
|
-
|
|
113
|
-
`workspace/DISCOCLAW.md` is no longer a managed or authoritative instruction source.
|
|
114
|
-
If you still have a legacy copy, treat it as historical reference only.
|
|
115
|
-
|
|
116
|
-
### Message batching
|
|
117
|
-
|
|
118
|
-
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.
|
|
119
|
-
|
|
120
|
-
### OpenRouter
|
|
121
|
-
|
|
122
|
-
Set `PRIMARY_RUNTIME=openrouter` to route requests through [OpenRouter](https://openrouter.ai), which provides access to models from Anthropic, OpenAI, Google, and others via a single API key.
|
|
123
|
-
|
|
124
|
-
Required: `OPENROUTER_API_KEY`. Optional overrides: `OPENROUTER_BASE_URL` (default: `https://openrouter.ai/api/v1`), `OPENROUTER_MODEL` (default: `anthropic/claude-sonnet-4.6`), and `OPENROUTER_PROVIDER_PREFERENCES` (OpenRouter-only JSON routed through the request `provider` field). Treat `.env` presence, `PRIMARY_RUNTIME=openrouter`, and `!models set chat openrouter` as config/routing intent only until the running instance proves the shipped OpenRouter path with `!status` or the startup credential report showing `openrouter-key: ok`.
|
|
125
|
-
|
|
126
|
-
For source checkouts, workload evidence beyond that key-visibility proof exists only through the repo smoke path, and read-only tool coverage is the only validated tool subset today. DiscoClaw now ships built-in OpenRouter tier defaults: `fast → openai/gpt-5-mini`, `capable → anthropic/claude-sonnet-4.6`, `deep → anthropic/claude-opus-4.6`. Override them with `DISCOCLAW_TIER_OPENROUTER_<TIER>` only when you need instance-specific routing. See [docs/audit/openrouter-api-key-support-boundary.md](docs/audit/openrouter-api-key-support-boundary.md) for the support boundary that npm installs and source checkouts both ship.
|
|
127
|
-
|
|
128
|
-
## Model Overrides
|
|
129
|
-
|
|
130
|
-
The `!models` command lets you view and swap AI models per role at runtime — no restart needed. Per-role model values persist to `models.json` under the data dir, while fast/voice runtime overlays persist separately to `runtime-overrides.json`. Live runtime swaps like `!models set chat openrouter` are still live-only until the next restart, but they affect the main runtime path broadly, not just chat.
|
|
131
|
-
|
|
132
|
-
For the full operator guide to install-mode detection, persistent adapter switches, OpenRouter tier overrides, fast/voice runtime behavior, and `!models reset` semantics, see [docs/runtime-switching.md](docs/runtime-switching.md).
|
|
133
|
-
|
|
134
|
-
**Roles:** `chat`, `fast`, `forge-drafter`, `forge-auditor`, `summary`, `cron`, `cron-exec`, `voice`
|
|
135
|
-
|
|
136
|
-
| Command | Description |
|
|
137
|
-
|---------|-------------|
|
|
138
|
-
| `!models` | Show current model assignments |
|
|
139
|
-
| `!models set <role> <model>` | Change the model for a role |
|
|
140
|
-
| `!models reset` | Revert all roles to startup defaults and clear overrides |
|
|
141
|
-
| `!models reset <role>` | Revert a specific role to its startup default |
|
|
142
|
-
|
|
143
|
-
**Examples:**
|
|
144
|
-
- `!models set chat claude-sonnet-4` — use Sonnet for chat
|
|
145
|
-
- `!models set chat openrouter` — live-switch the main runtime to OpenRouter until restart
|
|
146
|
-
- `!models set cron-exec haiku` — run crons on a cheaper model
|
|
147
|
-
- `!models set cron-exec default` — clear the cron-exec override and use the startup default again
|
|
148
|
-
- `!models set voice sonnet` — use a specific model for voice
|
|
149
|
-
- `!models reset` — clear all overrides and revert to startup defaults
|
|
150
|
-
|
|
151
|
-
Setting `chat` to a runtime name (`openrouter`, `openai`, `gemini-api`, `codex-cli`, `claude-cli`) live-switches the main runtime path until restart; setting `voice` to a runtime name switches only voice. Legacy aliases (`claude`, `codex`, `anthropic`) are still accepted. Exact model-string runtime auto-switching is only implemented for `fast` and `voice`.
|
|
152
|
-
|
|
153
|
-
## Secret Management
|
|
154
|
-
|
|
155
|
-
The `!secret` command lets you manage `.env` entries from Discord without touching the file directly. It works in DMs only — values are never echoed back.
|
|
156
|
-
|
|
157
|
-
| Command | Description |
|
|
158
|
-
|---------|-------------|
|
|
159
|
-
| `!secret set KEY=value` | Add or update a `.env` entry |
|
|
160
|
-
| `!secret unset KEY` | Remove a `.env` entry |
|
|
161
|
-
| `!secret list` | List key names in `.env` (values hidden) |
|
|
162
|
-
| `!secret help` | Show usage |
|
|
163
|
-
|
|
164
|
-
Changes take effect after a restart (`!restart`). Writes are atomic — a partial write can't corrupt your `.env`.
|
|
165
|
-
|
|
166
|
-
## Customization
|
|
167
|
-
|
|
168
|
-
### Shareable integration recipes
|
|
169
|
-
|
|
170
|
-
DiscoClaw supports a shareable markdown recipe format for passing integrations between users:
|
|
171
|
-
|
|
172
|
-
- Spec: `docs/discoclaw-recipe-spec.md`
|
|
173
|
-
- Template: `templates/recipes/integration.discoclaw-recipe.md`
|
|
174
|
-
- Example files: `recipes/examples/*.discoclaw-recipe.md`
|
|
175
|
-
- Skills:
|
|
176
|
-
- `skills/discoclaw-recipe-generator/SKILL.md`
|
|
177
|
-
- `skills/discoclaw-recipe-consumer/SKILL.md`
|
|
178
|
-
- Install/refresh invocable skill symlinks:
|
|
179
|
-
- `pnpm claude:install-skills`
|
|
42
|
+
Real-time voice with STT (Deepgram), TTS (Cartesia), barge-in, and transcript mirroring. Off by default. [Setup guide →](docs/voice.md)
|
|
180
43
|
|
|
181
|
-
|
|
44
|
+
## Self-management
|
|
182
45
|
|
|
183
|
-
|
|
46
|
+
Self-update from Discord (`!update apply`), health checks (`!doctor`), secret management (`!secret`), runtime model switching (`!models`), and restart (`!restart`) — no SSH needed.
|
|
184
47
|
|
|
185
|
-
When using the Claude runtime, you can connect external tool servers via MCP. Place a `.mcp.json` file in your workspace directory to configure servers — their tools become available during conversations. See [docs/mcp.md](docs/mcp.md) for the config format, examples, and troubleshooting.
|
|
186
|
-
|
|
187
|
-
## Managed browser launcher
|
|
188
|
-
|
|
189
|
-
Browser automation uses a Discoclaw-managed Chrome/Chromium profile directly. There is no companion service, long-lived helper daemon, or localhost control service to keep running in the background.
|
|
190
|
-
|
|
191
|
-
Use the launcher flow when you need a real browser profile for login-gated sites or later headless reuse:
|
|
192
|
-
|
|
193
|
-
- `discoclaw browser setup` checks storage rules, browser discovery, and current managed-profile readiness
|
|
194
|
-
- `discoclaw browser launch` opens the managed profile headed for one-time login and verifies CDP against that same browser instance
|
|
195
|
-
- `discoclaw browser launch --headless` reuses the same verified profile later without opening a visible window
|
|
196
|
-
- `!browser help` in Discord prints the same operator-facing setup, doctor, and launch commands
|
|
197
|
-
|
|
198
|
-
For source checkouts, repo-local managed browser storage is supported only at the ignored default `data/browser/` path. Custom in-repo data dirs are refused for this feature; if you need a different browser data location, move `DISCOCLAW_DATA_DIR` outside the repo.
|
|
199
|
-
|
|
200
|
-
## Prerequisites
|
|
201
|
-
|
|
202
|
-
**End users:**
|
|
203
|
-
- **Node.js >=20** — check with `node --version`
|
|
204
|
-
- One primary runtime:
|
|
205
|
-
- **Claude CLI** on your `PATH` — check with `claude --version` (see [Claude CLI docs](https://docs.anthropic.com/en/docs/claude-code) to install), or
|
|
206
|
-
- **Gemini API** via `PRIMARY_RUNTIME=gemini-api` plus `GEMINI_API_KEY`, or
|
|
207
|
-
- **Codex CLI** on your `PATH` — check with `codex --version` (binary presence only; session auth is a separate proof gate), or
|
|
208
|
-
- **OpenAI-compatible API key** via `OPENAI_API_KEY` (config presence only; live auth is a separate proof gate), or
|
|
209
|
-
- **OpenRouter API key** via `OPENROUTER_API_KEY` (config presence only until `!status` or the startup credential report shows `openrouter-key: ok`)
|
|
210
|
-
- Runtime-specific access for your chosen provider (Anthropic access for Claude CLI, Google API access for `gemini-api`, OpenAI access for Codex/OpenAI models)
|
|
211
|
-
|
|
212
|
-
1.0 provider/auth policy: `Claude CLI` on a source checkout is the explicitly supported default path, and `Codex CLI` on a source checkout is the explicitly supported secondary path. For every current provider/auth path, including `openai`, `openrouter`, `gemini-api`, and direct Anthropic, use the consolidated verdicts in [docs/audit/provider-auth-1.0-matrix.md](docs/audit/provider-auth-1.0-matrix.md) rather than inferring readiness from setup/config alone.
|
|
213
|
-
|
|
214
|
-
`discoclaw init` scaffolds Claude CLI, Codex CLI, OpenAI, OpenRouter, and Gemini API runtime paths.
|
|
215
|
-
|
|
216
|
-
For Codex and OpenAI paths, treat binary/key presence as readiness prerequisites only. The install-mode-specific support claims live in the [provider/auth 1.0 matrix](docs/audit/provider-auth-1.0-matrix.md), [Codex source-checkout audit](docs/audit/codex-blank-machine-readiness.md), and [Codex npm-managed audit](docs/audit/codex-npm-managed-path.md). For OpenRouter, the shipped support claim is narrower: [docs/audit/openrouter-api-key-support-boundary.md](docs/audit/openrouter-api-key-support-boundary.md) documents only the runtime-visible env-key proof boundary, with source-checkout workload evidence limited to the repo smoke path.
|
|
217
|
-
|
|
218
|
-
**Contributors (from source):**
|
|
219
|
-
- Everything above, plus **pnpm** — enable via Corepack (`corepack enable`) or install separately
|
|
220
|
-
|
|
221
|
-
### Model capability requirement
|
|
222
|
-
|
|
223
|
-
DiscoClaw assumes reliable structured output for several runtime paths (for example: Discord actions, cron JSON routing, and tool-call loops).
|
|
224
|
-
|
|
225
|
-
- For OpenAI-compatible and OpenRouter adapters, pick models that reliably support JSON-shaped output and function calling.
|
|
226
|
-
- For OpenRouter-backed source-checkout workloads, treat repo smoke-path validation as the workload proof surface, and treat read-only tool coverage as the only validated tool subset today.
|
|
227
|
-
- "OpenAI-compatible" API shape alone is not a capability guarantee.
|
|
228
|
-
- If a model fails JSON/tool-call smoke tests, treat it as unsupported for DiscoClaw runtime use.
|
|
229
|
-
- Use the [model validation smoke test checklist](docs/configuration.md#model-validation-smoke-test-recommended) before adopting a new model.
|
|
230
|
-
|
|
231
|
-
<!-- source-of-truth: docs/discord-bot-setup.md -->
|
|
232
48
|
## Quick start
|
|
233
49
|
|
|
234
|
-
### Discord setup (private server + bot)
|
|
235
|
-
|
|
236
|
-
1. Create a **private Discord server** dedicated to DiscoClaw (not a shared/public server).
|
|
237
|
-
2. In the [Discord Developer Portal](https://discord.com/developers/applications), create an application, then go to **Bot** -> **Add Bot**.
|
|
238
|
-
3. Under **Bot** -> **Privileged Gateway Intents**, enable **Message Content Intent**.
|
|
239
|
-
4. Copy the bot token and set it in `.env` as `DISCORD_TOKEN=...`.
|
|
240
|
-
5. Invite the bot to your server:
|
|
241
|
-
- Go to **Installation** (left sidebar) → set **Install Link** to **Discord Provided Link**
|
|
242
|
-
- Under **Default Install Settings**, add scope `bot` under Guild Install
|
|
243
|
-
- A **Permissions** selector appears. For a private server, pick `Administrator` — it's one selection and covers everything. For tighter permissions, see the [permission profiles](docs/discord-bot-setup.md#permission-profiles-choose-intentionally) in the full guide.
|
|
244
|
-
- **Save Changes**, then open the install link, pick your server, and authorize
|
|
245
|
-
6. In Discord, enable **Developer Mode** (User Settings -> Advanced), then copy IDs and set:
|
|
246
|
-
- `DISCORD_ALLOW_USER_IDS=<your user id>` (required; fail-closed if empty)
|
|
247
|
-
- `DISCORD_GUILD_ID=<server id>` (required for `pnpm release:rehearsal`; also required for auto-creating forum channels)
|
|
248
|
-
|
|
249
|
-
Full step-by-step guide: [docs/discord-bot-setup.md](docs/discord-bot-setup.md)
|
|
250
|
-
|
|
251
|
-
## Documentation
|
|
252
|
-
|
|
253
|
-
### Getting Started
|
|
254
|
-
|
|
255
|
-
- [Discord bot setup](docs/discord-bot-setup.md) — create a bot, invite it, configure permissions
|
|
256
|
-
- [MCP (Model Context Protocol)](docs/mcp.md) — connect external tool servers
|
|
257
|
-
|
|
258
|
-
### Features & Usage
|
|
259
|
-
|
|
260
|
-
- [Memory system](docs/memory.md) — five-layer memory architecture, tuning, and troubleshooting
|
|
261
|
-
- [Plan & Forge](docs/plan-and-forge.md) — autonomous planning and code generation
|
|
262
|
-
- [Discord actions](docs/discord-actions.md) — channels, messaging, moderation, tasks, crons
|
|
263
|
-
- [Cron / automations](docs/cron.md) — recurring task setup, advanced options, debugging
|
|
264
|
-
- [Tasks](docs/tasks.md) — task lifecycle, bidirectional sync, tag maps
|
|
265
|
-
- [Voice](docs/voice.md) — real-time voice chat setup (STT/TTS)
|
|
266
|
-
- [Shareable recipes](docs/discoclaw-recipe-spec.md) — integration recipe format spec
|
|
267
|
-
|
|
268
|
-
### Development
|
|
269
|
-
|
|
270
|
-
- [Philosophy](docs/philosophy.md) — design principles and trade-offs
|
|
271
|
-
- [Releasing](docs/releasing.md) — npm publish workflow and versioning
|
|
272
|
-
- [Inventory](docs/INVENTORY.md) — full component inventory and MVP status
|
|
273
|
-
|
|
274
|
-
### Operations
|
|
275
|
-
|
|
276
|
-
- [Configuration reference](docs/configuration.md) — all environment variables indexed by category
|
|
277
|
-
- [Provider/auth 1.0 matrix](docs/audit/provider-auth-1.0-matrix.md) — intended default path, supported secondary path, and the current `SUPPORTED FOR 1.0` / `PARTIAL` / `OUT OF SCOPE` verdicts for every implied provider/auth path
|
|
278
|
-
- [Claude source-checkout status](CLAUDE%20SOURCE-CHECKOUT%20STATUS.md) — current closeout memo for the Claude source-checkout path, including what the latest throwaway-checkout rerun did and did not prove
|
|
279
|
-
- [Claude source-checkout audit](docs/audit/claude-blank-machine-readiness.md) — current 1.0 verdict for the repo-owned `pnpm preflight*` + `pnpm claude:auth-smoke` path
|
|
280
|
-
- [Claude npm-managed audit](docs/audit/claude-npm-managed-path.md) — current 1.0 verdict for `npm install -g discoclaw`, `discoclaw init`, and the daemon/runtime-path gap
|
|
281
|
-
- [Codex source-checkout audit](docs/audit/codex-blank-machine-readiness.md) — current 1.0 verdict for the repo-owned `pnpm preflight*` path plus the separate Codex/OpenAI proof gates
|
|
282
|
-
- [Codex npm-managed audit](docs/audit/codex-npm-managed-path.md) — current 1.0 verdict for `npm install -g discoclaw`, manual Codex login proof, optional OpenAI fast-path proof, and the daemon/runtime-path gap
|
|
283
|
-
- [Managed browser launcher guide](#managed-browser-launcher) — dedicated profile flow, headed login, verified CDP handoff, and storage rules
|
|
284
|
-
- [Runtime/model switching](docs/runtime-switching.md) — operator guide for switching adapters, models, and defaults safely
|
|
285
|
-
- [Webhook exposure](docs/webhook-exposure.md) — tunnel/proxy setup and webhook security
|
|
286
|
-
- [Data migration](docs/data-migration.md) — migrating task data between formats
|
|
287
|
-
|
|
288
|
-
### Install and run
|
|
289
|
-
|
|
290
|
-
1. **Install globally:**
|
|
291
|
-
```bash
|
|
292
|
-
npm install -g discoclaw
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
> **Fedora 43+ / GCC 14+ — `@discordjs/opus` build failure (resolved)**
|
|
296
|
-
>
|
|
297
|
-
> This was fixed upstream in `@discordjs/opus` 0.10.0. If you are pinned to an older version, set the flag before installing:
|
|
298
|
-
> ```bash
|
|
299
|
-
> CFLAGS="-Wno-error=incompatible-pointer-types" npm install -g discoclaw
|
|
300
|
-
> ```
|
|
301
|
-
|
|
302
|
-
2. **Run the interactive setup wizard** (creates `.env` and scaffolds your workspace):
|
|
303
|
-
```bash
|
|
304
|
-
discoclaw init
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
3. **Register the system service:**
|
|
308
|
-
```bash
|
|
309
|
-
discoclaw install-daemon
|
|
310
|
-
```
|
|
311
|
-
Optional: pass `--service-name <name>` to use a custom service name (useful on macOS when running multiple instances, or to match your own naming convention):
|
|
312
|
-
```bash
|
|
313
|
-
discoclaw install-daemon --service-name personal
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
4. **Open the local operator dashboard:**
|
|
317
|
-
```bash
|
|
318
|
-
discoclaw dashboard
|
|
319
|
-
```
|
|
320
|
-
By default this listens on `127.0.0.1`. To reach it from a phone over Tailscale,
|
|
321
|
-
set `DISCOCLAW_DASHBOARD_TRUSTED_HOSTS` to your tailnet IP or MagicDNS hostname.
|
|
322
|
-
See [docs/dashboard-tailscale.md](docs/dashboard-tailscale.md).
|
|
323
|
-
|
|
324
|
-
If you are validating runtime auth, keep the install mode and auth method separate:
|
|
325
|
-
|
|
326
|
-
- Claude: global installs use the npm-managed path (`discoclaw init` guidance plus `discoclaw claude auth-smoke`), while source checkouts use `pnpm release:rehearsal` for the blessed full-path rehearsal or collect the same gates manually in order with `pnpm preflight:blank-machine`, `pnpm claude:auth-smoke`, `pnpm discord:smoke-test -- --guild-id <repo-local DISCORD_GUILD_ID>`, and `pnpm build`.
|
|
327
|
-
- Codex from source: `pnpm preflight*` is config-only; prove the Codex session separately with `codex exec ...`, and prove any OpenAI fast/alternate path separately with `OPENAI_SMOKE_TEST_TIERS=... pnpm test`.
|
|
328
|
-
- Codex from npm/global install: `discoclaw doctor` is config-only and no shipped `discoclaw codex auth-smoke` exists yet; use the same-shell `codex exec --skip-git-repo-check -- "Reply with OK"` login check, then confirm `openai-key: ok` separately if you enabled an OpenAI fast/alternate path.
|
|
329
|
-
|
|
330
|
-
Current npm-managed blocker for both Claude and Codex daemon claims: `discoclaw install-daemon` still renders services with `/usr/bin/node` and a fixed service `PATH`. The interactive shell check can therefore pass while the installed daemon resolves different Node or runtime binaries.
|
|
331
|
-
|
|
332
|
-
#### From source (contributors)
|
|
333
|
-
|
|
334
50
|
```bash
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
# Or manually: cp .env.example .env and fill in required vars:
|
|
339
|
-
# DISCORD_TOKEN
|
|
340
|
-
# DISCORD_ALLOW_USER_IDS
|
|
341
|
-
# For all ~90 options: cp .env.example.full .env
|
|
342
|
-
pnpm preflight:blank-machine
|
|
343
|
-
pnpm claude:auth-smoke # if PRIMARY_RUNTIME=claude-cli, before login, from a shell/account with no active Claude session
|
|
344
|
-
claude # if PRIMARY_RUNTIME=claude-cli, complete login in that same shell/account
|
|
345
|
-
pnpm claude:auth-smoke # if PRIMARY_RUNTIME=claude-cli, rerun after login in that same shell/account
|
|
346
|
-
pnpm release:rehearsal # blessed Claude 1.0 source-checkout rehearsal; requires repo-local .env with PRIMARY_RUNTIME=claude-cli
|
|
347
|
-
codex exec -m gpt-5.4 --skip-git-repo-check --ephemeral -s read-only -- "Reply with OK" # if PRIMARY_RUNTIME=codex-cli
|
|
348
|
-
OPENAI_SMOKE_TEST_TIERS=fast pnpm test # if any source-checkout path routes through OpenAI
|
|
349
|
-
pnpm build && pnpm dev
|
|
51
|
+
npm install -g discoclaw
|
|
52
|
+
discoclaw init
|
|
53
|
+
discoclaw install-daemon
|
|
350
54
|
```
|
|
351
55
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
`pnpm release:rehearsal` is the blessed source-checkout entrypoint for the full Claude 1.0 operational rehearsal. Its source of truth is the checkout's own `.env`: the harness reads the repo-local file, requires `PRIMARY_RUNTIME=claude-cli`, strips repo-local persistence/config path overrides that would escape the rehearsal temp root, and then applies explicit child-process overrides for `DISCOCLAW_DATA_DIR`, `WORKSPACE_CWD`, `GROUPS_DIR`, `BEADS_DIR`, and the rehearsal task prefix. The live operator loop it holds includes the first reply, a same-conversation follow-up reply, task sync, cron execution, and restart/recovery. Fresh-clone and first-login stranger evidence are established separately by the fresh-clone QA and auth-smoke path above; the rehearsal harness accepts checkout provenance as operator context when supplied, but it does not try to prove or gate on that provenance in code.
|
|
355
|
-
|
|
356
|
-
If `PRIMARY_RUNTIME=claude-cli`, run `pnpm preflight:blank-machine`, capture the separate Claude auth evidence in the order shown above, then run `pnpm discord:smoke-test -- --guild-id <repo-local DISCORD_GUILD_ID>` and `pnpm build` before `pnpm dev`, or run the full `pnpm release:rehearsal` harness once the repo-local `.env` is ready. If `PRIMARY_RUNTIME=codex-cli`, treat `pnpm preflight:blank-machine` as config/bootstrap evidence only, then capture separate Codex session-auth evidence with the `codex exec ...` prompt. If any source-checkout route uses OpenAI, capture separate `OPENAI_API_KEY` evidence with `OPENAI_SMOKE_TEST_TIERS=... pnpm test`.
|
|
357
|
-
|
|
358
|
-
### Claude runtime validation
|
|
359
|
-
|
|
360
|
-
Source-checkout 1.0 support status: `SUPPORTED FOR 1.0` for the repo-owned Claude path within the audited boundary: the same no-session shell or account recorded the pre-login unauthenticated result, completed interactive Claude CLI login, and then passed the post-login `pnpm claude:auth-smoke` rerun. The current closeout memo is [Claude source-checkout status](CLAUDE%20SOURCE-CHECKOUT%20STATUS.md), and the deeper audit record remains [docs/audit/claude-blank-machine-readiness.md](docs/audit/claude-blank-machine-readiness.md).
|
|
361
|
-
|
|
362
|
-
Npm-managed 1.0 audit verdict: `NOT YET SUPPORT-CLAIMABLE`. The installed CLI now has a shell-level Claude check, but the daemon path still is not claimable because `discoclaw install-daemon` hardcodes `/usr/bin/node` plus a fixed service `PATH`, and `discoclaw init` does not persist `CLAUDE_BIN`. See [docs/audit/claude-npm-managed-path.md](docs/audit/claude-npm-managed-path.md).
|
|
363
|
-
|
|
364
|
-
1. Create a throwaway clone, run `pnpm install --frozen-lockfile`, and supply a real clone-local `.env` (`pnpm run setup` or copy from `.env.example` / `.env.example.full`).
|
|
365
|
-
2. If you are validating from a machine that already has DiscoClaw or Claude state, isolate `DISCOCLAW_DATA_DIR`, `WORKSPACE_CWD`, `GROUPS_DIR`, and `BEADS_DIR` to throwaway paths before claiming stranger-run evidence.
|
|
366
|
-
3. Run the automated config/bootstrap check:
|
|
367
|
-
```bash
|
|
368
|
-
pnpm preflight:blank-machine
|
|
369
|
-
```
|
|
370
|
-
Optional:
|
|
371
|
-
```bash
|
|
372
|
-
pnpm preflight:blank-machine:online
|
|
373
|
-
```
|
|
374
|
-
4. Treat the result correctly:
|
|
375
|
-
- `pnpm preflight:blank-machine` proves the local prerequisites against the current `.env` only, ignoring inherited shell env from the host machine.
|
|
376
|
-
- `pnpm preflight:blank-machine:online` adds a live Discord login/gateway-intent check on top of that same blank-machine env boundary.
|
|
377
|
-
- Plain `pnpm preflight` keeps its broader contributor-oriented behavior and can still be useful for checking the current shell environment.
|
|
378
|
-
- Neither command proves Claude login/auth state.
|
|
379
|
-
5. From a shell or account with no active Claude session, run the required pre-login failure check:
|
|
380
|
-
```bash
|
|
381
|
-
pnpm claude:auth-smoke
|
|
382
|
-
```
|
|
383
|
-
Confirm it returns `Claude CLI appears installed but not authenticated.`
|
|
384
|
-
6. Log in interactively in that same shell or account:
|
|
385
|
-
```bash
|
|
386
|
-
claude
|
|
387
|
-
```
|
|
388
|
-
7. Rerun the happy-path check in that same shell or account:
|
|
389
|
-
```bash
|
|
390
|
-
pnpm claude:auth-smoke
|
|
391
|
-
```
|
|
392
|
-
Confirm it returns `Claude CLI answered the minimal prompt.`
|
|
393
|
-
8. If step 7 was captured only from an already-logged-in shell or account, record that limitation and downgrade the claim to `fresh-clone post-login path only`.
|
|
394
|
-
9. For the full blessed source-checkout path, run the rehearsal harness and record whether the checkout was throwaway or reused:
|
|
395
|
-
```bash
|
|
396
|
-
pnpm release:rehearsal
|
|
397
|
-
```
|
|
398
|
-
The harness enforces the repo-local `.env` plus explicit child-process isolation overrides, writes a closeout under `docs/release-audit/`, and treats unresolved rehearsal-owned cleanup as a blocked verdict.
|
|
399
|
-
10. Start DiscoClaw after the source-checkout gates pass:
|
|
400
|
-
```bash
|
|
401
|
-
pnpm build && pnpm dev
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Codex runtime validation
|
|
405
|
-
|
|
406
|
-
Codex 1.0 support matrix:
|
|
407
|
-
|
|
408
|
-
| Install mode | Config/bootstrap evidence only | Separate auth proof gates | 1.0 verdict |
|
|
409
|
-
| --- | --- | --- | --- |
|
|
410
|
-
| Source checkout | `pnpm preflight:blank-machine` / `pnpm preflight` prove local prerequisites only. They do not invoke Codex or OpenAI. | 1. Prove the Codex CLI session in the same shell with `codex exec -m gpt-5.4 --skip-git-repo-check --ephemeral -s read-only -- "Reply with OK"` before and after `codex` login. 2. If any source-checkout path routes through OpenAI, run `OPENAI_SMOKE_TEST_TIERS=fast pnpm test` or replace `fast` with your intended tier/model. | `PASS` for the repo-owned source path. See [docs/audit/codex-blank-machine-readiness.md](docs/audit/codex-blank-machine-readiness.md). |
|
|
411
|
-
| npm / global install | `discoclaw doctor`, `!doctor`, and `discoclaw init` detection stay config-only. No shipped `discoclaw codex auth-smoke` exists yet. | 1. Run `codex exec --skip-git-repo-check -- "Reply with OK"` before and after `codex` login in the same installed shell. 2. If you enabled an OpenAI fast/alternate path, start DiscoClaw and confirm `!status` or the startup credential report shows `openai-key: ok`. | `NOT YET SUPPORT-CLAIMABLE` for the daemon path. See [docs/audit/codex-npm-managed-path.md](docs/audit/codex-npm-managed-path.md). |
|
|
412
|
-
|
|
413
|
-
Do not treat `codex --version`, `OPENAI_API_KEY` presence, `pnpm preflight*`, or `discoclaw doctor` as full Codex readiness proof by themselves. Those are prerequisite/config surfaces only.
|
|
414
|
-
|
|
415
|
-
## Updating
|
|
416
|
-
|
|
417
|
-
DiscoClaw can check for and apply updates from inside Discord — no SSH or terminal needed.
|
|
418
|
-
|
|
419
|
-
| Command | Description |
|
|
420
|
-
|---------|-------------|
|
|
421
|
-
| `!update` | Check if a newer version is available on npm |
|
|
422
|
-
| `!update apply` | Download the update, reinstall, and restart the service |
|
|
423
|
-
| `!update audit` | Show npm-managed runtime audit details |
|
|
424
|
-
| `!update help` | Show usage |
|
|
425
|
-
|
|
426
|
-
**Global install (from Discord):**
|
|
427
|
-
|
|
428
|
-
```
|
|
429
|
-
!update apply
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
**Global install (from the command line):**
|
|
433
|
-
|
|
434
|
-
```bash
|
|
435
|
-
npm update -g discoclaw
|
|
436
|
-
discoclaw install-daemon # re-register the service after updating
|
|
437
|
-
# If you used a custom service name, pass it again:
|
|
438
|
-
# discoclaw install-daemon --service-name personal
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
For Claude on npm-managed installs, keep using the installed-shell validation path after updates: rerun `discoclaw init` if you need the manual login guidance, and use `discoclaw claude auth-smoke` if you want the shipped shell-level smoke check. Do not treat `pnpm preflight*` or `pnpm claude:auth-smoke` as npm-managed evidence; those remain source-checkout-only. Re-registering the daemon also does not remove the current service-path blocker: the generated service still pins `/usr/bin/node` and a fixed `PATH`, so a passing interactive shell smoke test does not yet prove the daemon will resolve the same Node and Claude binaries.
|
|
442
|
-
|
|
443
|
-
For Codex on npm-managed installs, `discoclaw doctor` remains config-only and there is still no shipped `discoclaw codex auth-smoke`. Repeat the same-shell `codex exec --skip-git-repo-check -- "Reply with OK"` login check after updates, and if you enabled an OpenAI fast/alternate path confirm the restarted instance reports `openai-key: ok`. Re-registering the daemon still does not remove the service-path blocker: the generated service pins `/usr/bin/node` and a fixed `PATH`, and init does not persist `CODEX_BIN`, so interactive shell success is not yet daemon-proof.
|
|
56
|
+
You'll need a [private Discord server and bot token](docs/discord-bot-setup.md) and at least one AI runtime ([configuration reference](docs/configuration.md)).
|
|
444
57
|
|
|
445
58
|
**From source:**
|
|
446
59
|
|
|
447
60
|
```bash
|
|
448
|
-
git
|
|
449
|
-
pnpm install
|
|
450
|
-
pnpm
|
|
61
|
+
git clone https://github.com/DiscoClaw/discoclaw.git && cd discoclaw
|
|
62
|
+
pnpm install --frozen-lockfile
|
|
63
|
+
pnpm run setup
|
|
64
|
+
pnpm build && pnpm dev
|
|
451
65
|
```
|
|
452
66
|
|
|
453
|
-
|
|
67
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor setup including runtime validation.
|
|
454
68
|
|
|
455
|
-
|
|
69
|
+
## Documentation
|
|
456
70
|
|
|
457
|
-
|
|
71
|
+
**Getting started:** [Discord bot setup](docs/discord-bot-setup.md) · [Configuration](docs/configuration.md) · [MCP](docs/mcp.md)
|
|
458
72
|
|
|
459
|
-
|
|
73
|
+
**Features:** [Memory](docs/memory.md) · [Tasks](docs/tasks.md) · [Crons](docs/cron.md) · [Voice](docs/voice.md) · [Discord actions](docs/discord-actions.md) · [Plan & Forge](docs/plan-and-forge.md) · [Browser automation](docs/browser.md) · [Recipes](docs/discoclaw-recipe-spec.md)
|
|
460
74
|
|
|
461
|
-
|
|
75
|
+
**Operations:** [Runtime switching](docs/runtime-switching.md) · [Dashboard](docs/dashboard-tailscale.md) · [Webhook exposure](docs/webhook-exposure.md) · [Data migration](docs/data-migration.md)
|
|
462
76
|
|
|
463
|
-
|
|
77
|
+
**Audits:** [Provider/auth matrix](docs/audit/provider-auth-1.0-matrix.md) · [Claude](docs/audit/claude-blank-machine-readiness.md) · [Codex](docs/audit/codex-blank-machine-readiness.md)
|
|
464
78
|
|
|
465
|
-
|
|
466
|
-
systemctl --user restart discoclaw.service
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
Then verify the recovery path in order:
|
|
470
|
-
|
|
471
|
-
1. Confirm the service is back:
|
|
472
|
-
```bash
|
|
473
|
-
systemctl --user status discoclaw.service
|
|
474
|
-
```
|
|
475
|
-
2. Inspect recent logs for a clean startup:
|
|
476
|
-
```bash
|
|
477
|
-
journalctl --user -u discoclaw.service -n 50 --no-pager
|
|
478
|
-
```
|
|
479
|
-
3. Send a short Discord message and confirm the already-validated runtime path answers normally after restart.
|
|
480
|
-
4. If a long-running reply was interrupted by the restart and `DISCOCLAW_COMPLETION_NOTIFY=1`, confirm startup recovery posts either:
|
|
481
|
-
- the persisted recovery summary text, or
|
|
482
|
-
- a generic completion notice ending with `Recovered after restart.`
|
|
483
|
-
5. Keep this evidence paired with the earlier Claude validation record; restart success does not close the separate first-login stranger gate by itself.
|
|
79
|
+
**Development:** [Philosophy](docs/philosophy.md) · [Releasing](docs/releasing.md) · [Inventory](docs/INVENTORY.md)
|
|
484
80
|
|
|
485
81
|
## Platform support
|
|
486
82
|
|
|
487
|
-
|
|
488
|
-
- **Linux** — systemd service file provided for production deployment (see `.context/ops.md`)
|
|
489
|
-
- **macOS / Windows** — use pm2, screen, or another process manager for long-running deployment; or just `pnpm dev` in a terminal
|
|
490
|
-
|
|
491
|
-
> Windows is not tested for production use in v0.x. The session scanner has known path-handling issues on Windows, and the Claude CLI primarily targets Linux and macOS.
|
|
83
|
+
Linux (systemd service included), macOS, Windows. Production daemon via systemd on Linux, or pm2/screen elsewhere.
|
|
492
84
|
|
|
493
85
|
## Safety
|
|
494
86
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
- Use a **private Discord server** — don't start in a shared or public server
|
|
498
|
-
- Use **least-privilege** Discord permissions
|
|
499
|
-
- Keep `DISCORD_ALLOW_USER_IDS` tight — this is the primary security boundary
|
|
500
|
-
- Empty allowlist = respond to nobody (fail-closed)
|
|
501
|
-
- Optionally restrict channels with `DISCORD_CHANNEL_IDS`
|
|
502
|
-
- External content (Discord messages, web pages, files) is **data**, not instructions
|
|
503
|
-
|
|
504
|
-
## Workspace layout
|
|
505
|
-
|
|
506
|
-
The orchestrator runs AI runtimes in a separate working directory (`WORKSPACE_CWD`), keeping the repo clean while giving your assistant a persistent workspace.
|
|
507
|
-
|
|
508
|
-
- Set `DISCOCLAW_DATA_DIR` to use `$DISCOCLAW_DATA_DIR/workspace` (good for Dropbox-backed setups)
|
|
509
|
-
- Or leave it unset to use `./workspace` relative to the repo
|
|
510
|
-
- Content (channel context, Discord config) defaults to `$DISCOCLAW_DATA_DIR/content`
|
|
511
|
-
|
|
512
|
-
## Development
|
|
513
|
-
|
|
514
|
-
```bash
|
|
515
|
-
pnpm preflight # automated prerequisite check; Claude auth remains manual
|
|
516
|
-
pnpm preflight:blank-machine # same check, but ignore inherited shell env and use only .env
|
|
517
|
-
pnpm preflight:online # adds live Discord login/intents validation
|
|
518
|
-
pnpm preflight:blank-machine:online # blank-machine check plus live Discord login/intents validation
|
|
519
|
-
pnpm claude:auth-smoke # Claude CLI login/auth smoke check
|
|
520
|
-
pnpm release:rehearsal # full Claude source-checkout release rehearsal
|
|
521
|
-
pnpm dev # start dev mode
|
|
522
|
-
pnpm build # compile TypeScript
|
|
523
|
-
pnpm test # run tests
|
|
524
|
-
```
|
|
87
|
+
Use a **private Discord server**, keep `DISCORD_ALLOW_USER_IDS` tight (fail-closed if empty), and use least-privilege Discord permissions. See [SECURITY.md](SECURITY.md).
|
|
525
88
|
|
|
526
89
|
## Built with
|
|
527
90
|
|
package/dist/config.js
CHANGED
|
@@ -704,6 +704,7 @@ export function parseConfig(env) {
|
|
|
704
704
|
summaryTargetRatio: parseZeroToOneExclusive(env, 'DISCOCLAW_SUMMARY_TARGET_RATIO', 0.65),
|
|
705
705
|
summaryDataDirOverride: parseTrimmedString(env, 'DISCOCLAW_SUMMARY_DATA_DIR'),
|
|
706
706
|
summaryArchiveDirOverride: parseTrimmedString(env, 'DISCOCLAW_SUMMARY_ARCHIVE_DIR'),
|
|
707
|
+
capsuleTtlMs: parseNonNegativeInt(env, 'DISCOCLAW_CAPSULE_TTL_MS', 7_200_000),
|
|
707
708
|
durableMemoryEnabled: parseBoolean(env, 'DISCOCLAW_DURABLE_MEMORY_ENABLED', true),
|
|
708
709
|
durableDataDirOverride: parseTrimmedString(env, 'DISCOCLAW_DURABLE_DATA_DIR'),
|
|
709
710
|
durableInjectMaxChars: parsePositiveInt(env, 'DISCOCLAW_DURABLE_INJECT_MAX_CHARS', 2000),
|
|
@@ -43,12 +43,20 @@ export const QUERY_ACTION_TYPES = new Set([
|
|
|
43
43
|
// Archive
|
|
44
44
|
'archiveList',
|
|
45
45
|
]);
|
|
46
|
+
// Actions that are not pure queries but still need a follow-up on success
|
|
47
|
+
// because they produce data the bot needs to act on (e.g. a downloaded file).
|
|
48
|
+
// Unlike QUERY_ACTION_TYPES, these also trigger follow-up on failure so the
|
|
49
|
+
// bot can report the error.
|
|
50
|
+
export const ALWAYS_FOLLOW_UP_TYPES = new Set([
|
|
51
|
+
'downloadAttachment',
|
|
52
|
+
]);
|
|
46
53
|
export function hasQueryAction(actionTypes) {
|
|
47
54
|
return actionTypes.some((t) => QUERY_ACTION_TYPES.has(t));
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* Returns true when a follow-up AI invocation should be triggered:
|
|
51
58
|
* - any query action succeeded (to process returned data), OR
|
|
59
|
+
* - any ALWAYS_FOLLOW_UP action ran (success or failure), OR
|
|
52
60
|
* - any non-query action failed (so the bot can explain the failure).
|
|
53
61
|
*
|
|
54
62
|
* Query action failures are excluded because there is no useful result data
|
|
@@ -58,12 +66,15 @@ export function shouldTriggerFollowUp(actions, results) {
|
|
|
58
66
|
const anyQuerySucceeded = actions.some((a, i) => QUERY_ACTION_TYPES.has(a.type) && results[i]?.ok);
|
|
59
67
|
if (anyQuerySucceeded)
|
|
60
68
|
return true;
|
|
69
|
+
const anyAlwaysFollowUp = actions.some((a) => ALWAYS_FOLLOW_UP_TYPES.has(a.type));
|
|
70
|
+
if (anyAlwaysFollowUp)
|
|
71
|
+
return true;
|
|
61
72
|
const anyNonQueryActionFailed = results.some((r, i) => r && !r.ok && !QUERY_ACTION_TYPES.has(actions[i]?.type ?? ''));
|
|
62
73
|
return anyNonQueryActionFailed;
|
|
63
74
|
}
|
|
64
75
|
/**
|
|
65
|
-
* Returns true when the follow-up was triggered exclusively by a
|
|
66
|
-
*
|
|
76
|
+
* Returns true when the follow-up was triggered exclusively by a failure
|
|
77
|
+
* — i.e. no query action succeeded and no ALWAYS_FOLLOW_UP action succeeded.
|
|
67
78
|
*
|
|
68
79
|
* Use this to select a failure-specific placeholder message and prompt suffix
|
|
69
80
|
* rather than the generic "following up..." variants.
|
|
@@ -72,6 +83,11 @@ export function isFailureFollowUp(actions, results) {
|
|
|
72
83
|
const anyQuerySucceeded = actions.some((a, i) => QUERY_ACTION_TYPES.has(a.type) && results[i]?.ok);
|
|
73
84
|
if (anyQuerySucceeded)
|
|
74
85
|
return false;
|
|
86
|
+
const anyAlwaysFollowUpSucceeded = actions.some((a, i) => ALWAYS_FOLLOW_UP_TYPES.has(a.type) && results[i]?.ok);
|
|
87
|
+
if (anyAlwaysFollowUpSucceeded)
|
|
88
|
+
return false;
|
|
89
|
+
// ALWAYS_FOLLOW_UP types are not in QUERY_ACTION_TYPES, so their failures
|
|
90
|
+
// are caught here alongside regular non-query failures.
|
|
75
91
|
return results.some((r, i) => r && !r.ok && !QUERY_ACTION_TYPES.has(actions[i]?.type ?? ''));
|
|
76
92
|
}
|
|
77
93
|
/**
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default TTL for continuation capsules: 2 hours.
|
|
3
|
+
* If the parent summary's updatedAt is older than this, the capsule is
|
|
4
|
+
* considered stale and should not be injected.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_CAPSULE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
7
|
+
/**
|
|
8
|
+
* Patterns in `currentFocus` that indicate the capsule carries no useful
|
|
9
|
+
* state — the assistant was idle, awaiting input, or had no active task.
|
|
10
|
+
*/
|
|
11
|
+
const IDLE_FOCUS_PATTERNS = [
|
|
12
|
+
/^idle$/i,
|
|
13
|
+
/^none$/i,
|
|
14
|
+
/^awaiting\b/i,
|
|
15
|
+
/^waiting\b/i,
|
|
16
|
+
/^no active task/i,
|
|
17
|
+
/^n\/a$/i,
|
|
18
|
+
/^\(none\)$/i,
|
|
19
|
+
/^\(idle\)$/i,
|
|
20
|
+
/^\(awaiting\b/i,
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the capsule's `currentFocus` matches an idle/no-state
|
|
24
|
+
* pattern, meaning the capsule should not be injected.
|
|
25
|
+
*/
|
|
26
|
+
export function isCapsuleIdle(capsule) {
|
|
27
|
+
const focus = capsule.currentFocus.trim();
|
|
28
|
+
return IDLE_FOCUS_PATTERNS.some(pattern => pattern.test(focus));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns true when the parent summary's `updatedAt` exceeds the staleness
|
|
32
|
+
* threshold relative to `now`.
|
|
33
|
+
*/
|
|
34
|
+
export function isCapsuleExpired(updatedAt, now = Date.now(), ttlMs = DEFAULT_CAPSULE_TTL_MS) {
|
|
35
|
+
if (ttlMs <= 0)
|
|
36
|
+
return false;
|
|
37
|
+
return now - updatedAt > ttlMs;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Combined check: returns whether a capsule should be injected into
|
|
41
|
+
* the conversation prompt. Both idle detection and TTL expiry are
|
|
42
|
+
* evaluated; idle is checked first (cheaper).
|
|
43
|
+
*/
|
|
44
|
+
export function validateCapsuleForInjection(capsule, updatedAt, opts) {
|
|
45
|
+
if (isCapsuleIdle(capsule)) {
|
|
46
|
+
return { valid: false, reason: 'idle' };
|
|
47
|
+
}
|
|
48
|
+
if (isCapsuleExpired(updatedAt, opts?.now, opts?.ttlMs)) {
|
|
49
|
+
return { valid: false, reason: 'expired' };
|
|
50
|
+
}
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_CAPSULE_TTL_MS, isCapsuleIdle, isCapsuleExpired, validateCapsuleForInjection, } from './capsule-invalidation.js';
|
|
3
|
+
function makeCapsule(currentFocus) {
|
|
4
|
+
return { currentFocus, nextStep: 'do something' };
|
|
5
|
+
}
|
|
6
|
+
describe('isCapsuleIdle', () => {
|
|
7
|
+
it.each([
|
|
8
|
+
'idle',
|
|
9
|
+
'Idle',
|
|
10
|
+
'IDLE',
|
|
11
|
+
'none',
|
|
12
|
+
'None',
|
|
13
|
+
'NONE',
|
|
14
|
+
'awaiting input',
|
|
15
|
+
'Awaiting user response',
|
|
16
|
+
'waiting for user',
|
|
17
|
+
'Waiting on feedback',
|
|
18
|
+
'no active task',
|
|
19
|
+
'No active task right now',
|
|
20
|
+
'n/a',
|
|
21
|
+
'N/A',
|
|
22
|
+
'(none)',
|
|
23
|
+
'(None)',
|
|
24
|
+
'(idle)',
|
|
25
|
+
'(Idle)',
|
|
26
|
+
'(awaiting input)',
|
|
27
|
+
'(Awaiting response)',
|
|
28
|
+
])('returns true for idle focus: %s', (focus) => {
|
|
29
|
+
expect(isCapsuleIdle(makeCapsule(focus))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it.each([
|
|
32
|
+
'Implement feature X',
|
|
33
|
+
'Debugging the parser',
|
|
34
|
+
'idling is not the same',
|
|
35
|
+
'nonetheless important',
|
|
36
|
+
])('returns false for active focus: %s', (focus) => {
|
|
37
|
+
expect(isCapsuleIdle(makeCapsule(focus))).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it('trims whitespace before matching', () => {
|
|
40
|
+
expect(isCapsuleIdle(makeCapsule(' idle '))).toBe(true);
|
|
41
|
+
expect(isCapsuleIdle(makeCapsule(' none '))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('isCapsuleExpired', () => {
|
|
45
|
+
it('returns false when within TTL', () => {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const updatedAt = now - (DEFAULT_CAPSULE_TTL_MS - 1000);
|
|
48
|
+
expect(isCapsuleExpired(updatedAt, now)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('returns true when past default TTL', () => {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const updatedAt = now - DEFAULT_CAPSULE_TTL_MS - 1;
|
|
53
|
+
expect(isCapsuleExpired(updatedAt, now)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('returns false at exactly the TTL boundary', () => {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const updatedAt = now - DEFAULT_CAPSULE_TTL_MS;
|
|
58
|
+
expect(isCapsuleExpired(updatedAt, now)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('respects a custom ttlMs', () => {
|
|
61
|
+
const now = 100_000;
|
|
62
|
+
expect(isCapsuleExpired(90_000, now, 5_000)).toBe(true); // 10s > 5s TTL
|
|
63
|
+
expect(isCapsuleExpired(96_000, now, 5_000)).toBe(false); // 4s < 5s TTL
|
|
64
|
+
});
|
|
65
|
+
it('returns false when ttlMs is 0 (TTL disabled)', () => {
|
|
66
|
+
const now = 100_000;
|
|
67
|
+
expect(isCapsuleExpired(1, now, 0)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('returns false when ttlMs is negative (TTL disabled)', () => {
|
|
70
|
+
const now = 100_000;
|
|
71
|
+
expect(isCapsuleExpired(1, now, -1)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('DEFAULT_CAPSULE_TTL_MS is 2 hours', () => {
|
|
74
|
+
expect(DEFAULT_CAPSULE_TTL_MS).toBe(2 * 60 * 60 * 1000);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('validateCapsuleForInjection', () => {
|
|
78
|
+
const now = 1_000_000;
|
|
79
|
+
const freshUpdatedAt = now - 60_000; // 1 minute ago
|
|
80
|
+
it('returns valid for active, fresh capsule', () => {
|
|
81
|
+
const capsule = makeCapsule('Implementing feature X');
|
|
82
|
+
const result = validateCapsuleForInjection(capsule, freshUpdatedAt, { now });
|
|
83
|
+
expect(result).toEqual({ valid: true });
|
|
84
|
+
});
|
|
85
|
+
it('returns idle reason for idle capsule', () => {
|
|
86
|
+
const capsule = makeCapsule('idle');
|
|
87
|
+
const result = validateCapsuleForInjection(capsule, freshUpdatedAt, { now });
|
|
88
|
+
expect(result).toEqual({ valid: false, reason: 'idle' });
|
|
89
|
+
});
|
|
90
|
+
it('returns expired reason for stale capsule', () => {
|
|
91
|
+
const capsule = makeCapsule('Implementing feature X');
|
|
92
|
+
const staleUpdatedAt = now - DEFAULT_CAPSULE_TTL_MS - 1;
|
|
93
|
+
const result = validateCapsuleForInjection(capsule, staleUpdatedAt, { now });
|
|
94
|
+
expect(result).toEqual({ valid: false, reason: 'expired' });
|
|
95
|
+
});
|
|
96
|
+
it('idle takes precedence over expired', () => {
|
|
97
|
+
const capsule = makeCapsule('none');
|
|
98
|
+
const staleUpdatedAt = now - DEFAULT_CAPSULE_TTL_MS - 1;
|
|
99
|
+
const result = validateCapsuleForInjection(capsule, staleUpdatedAt, { now });
|
|
100
|
+
expect(result).toEqual({ valid: false, reason: 'idle' });
|
|
101
|
+
});
|
|
102
|
+
it('respects custom ttlMs via opts', () => {
|
|
103
|
+
const capsule = makeCapsule('Working on task');
|
|
104
|
+
const updatedAt = now - 10_000; // 10s ago
|
|
105
|
+
const result = validateCapsuleForInjection(capsule, updatedAt, { now, ttlMs: 5_000 });
|
|
106
|
+
expect(result).toEqual({ valid: false, reason: 'expired' });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -19,6 +19,7 @@ import { resolveGroundedToolCapabilities } from '../runtime/tool-capabilities.js
|
|
|
19
19
|
import { fetchMessageHistory } from './message-history.js';
|
|
20
20
|
import { loadSummary, saveSummary, generateSummary, archiveSummary, recompressSummary, estimateSummaryTokens, buildConversationMemorySection, } from './summarizer.js';
|
|
21
21
|
import { parseCapsuleBlock } from './capsule.js';
|
|
22
|
+
import { validateCapsuleForInjection } from './capsule-invalidation.js';
|
|
22
23
|
import { parseMemoryCommand, handleMemoryCommand } from './memory-commands.js';
|
|
23
24
|
import { parseSecretCommand, handleSecretCommand } from './secret-commands.js';
|
|
24
25
|
import { parsePlanCommand, handlePlanCommand, preparePlanRun, handlePlanSkip, closePlanIfComplete, NO_PHASES_SENTINEL, findPlanFile, looksLikePlanId, PLAN_DISABLED_NUDGE } from './plan-commands.js';
|
|
@@ -2717,6 +2718,13 @@ export function createMessageCreateHandler(params, queue, statusRef) {
|
|
|
2717
2718
|
existingSummaryUpdatedAt = existing.updatedAt;
|
|
2718
2719
|
existingSummaryRegeneratedAt = existing.regeneratedAt;
|
|
2719
2720
|
existingContinuationCapsule = existing.continuationCapsule;
|
|
2721
|
+
if (existingContinuationCapsule) {
|
|
2722
|
+
const capsuleCheck = validateCapsuleForInjection(existingContinuationCapsule, existing.updatedAt, { ttlMs: params.capsuleTtlMs });
|
|
2723
|
+
if (!capsuleCheck.valid) {
|
|
2724
|
+
params.log?.info({ reason: capsuleCheck.reason, sessionKey }, 'discord:capsule skipped at injection');
|
|
2725
|
+
existingContinuationCapsule = undefined;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2720
2728
|
summarySection = buildConversationMemorySection(existingSummaryText, {
|
|
2721
2729
|
turnsSinceUpdate: existing.turnsSinceUpdate,
|
|
2722
2730
|
regeneratedAt: existing.regeneratedAt,
|
|
@@ -450,7 +450,7 @@ describe('image input precedence — direct > reply-ref > history', () => {
|
|
|
450
450
|
});
|
|
451
451
|
});
|
|
452
452
|
describe('manual message finalization guard', () => {
|
|
453
|
-
const PROMISED_ACTION_WARNING = 'Warning: this reply says Discord-managed work
|
|
453
|
+
const PROMISED_ACTION_WARNING = 'Warning: this reply says Discord-managed work was performed or is being performed';
|
|
454
454
|
beforeEach(() => {
|
|
455
455
|
vi.clearAllMocks();
|
|
456
456
|
resetAbortRegistry();
|
|
@@ -193,9 +193,11 @@ export function shouldSuppressFollowUp(processedText, actionsCount, imagesCount,
|
|
|
193
193
|
}
|
|
194
194
|
const DISCORD_ACTION_INTENT_BASE_VERBS = String.raw `create|send|edit|delete|close|open|post|react|launch|remember|forget|pin|unpin|crosspost|archive|ban|kick|timeout|set`;
|
|
195
195
|
const DISCORD_ACTION_INTENT_PROGRESSIVE_VERBS = String.raw `creating|sending|editing|deleting|closing|opening|posting|reacting|launching|remembering|forgetting|pinning|unpinning|crossposting|archiving|banning|kicking|setting`;
|
|
196
|
+
const DISCORD_ACTION_INTENT_PAST_VERBS = String.raw `posted|sent|edited|deleted|closed|opened|created|reacted|launched|remembered|forgot|pinned|unpinned|crossposted|archived|banned|kicked|set`;
|
|
197
|
+
const DISCORD_ACTION_INTENT_PAST_PARTICIPLES = String.raw `posted|sent|edited|deleted|closed|opened|created|reacted|launched|remembered|forgotten|pinned|unpinned|crossposted|archived|banned|kicked|set`;
|
|
196
198
|
const DISCORD_ACTION_INTENT_RESOURCE_NOUNS = String.raw `channel|thread|message|reply|task|plan|cron|poll|reaction|pin|user|member|nickname|status|activity|canvas|image|file|attachment|memory|preference|fact|note`;
|
|
197
199
|
const DISCORD_ACTION_INTENT_NEGATION_RE = /\b(?:i have not started yet|i haven't started yet|have not started yet|haven't started yet|not started yet|i have not begun yet|i haven't begun yet|have not begun yet|haven't begun yet|i am not starting|i'm not starting|i am not doing that yet|i'm not doing that yet|not proceeding now|not handling it now)\b/i;
|
|
198
|
-
const DISCORD_ACTION_INTENT_EXPLANATION_RE = /\b(?:example(?: only)?|for example|for instance|e\.g\.|i can|i could|i would|you can|you could|you would|if you want|when you're ready|would use|would emit|would send|would create|would run|do not run|don't run|not run)\b/i;
|
|
200
|
+
const DISCORD_ACTION_INTENT_EXPLANATION_RE = /\b(?:example(?: only)?|for example|for instance|e\.g\.|i can|i could|i would|you can|you could|you would|if you want|if I|when you're ready|would use|would emit|would send|would create|would run|do not run|don't run|not run)\b/i;
|
|
199
201
|
const DISCORD_ACTION_INTENT_PATTERNS = [
|
|
200
202
|
new RegExp(String.raw `\b(?:i am|i'm)\s+(?:${DISCORD_ACTION_INTENT_PROGRESSIVE_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b(?:[^.!?\n]{0,40}\b(?:now|already)\b)?`, 'i'),
|
|
201
203
|
new RegExp(String.raw `\b(?:i am|i'm)\s+going to\s+(?:${DISCORD_ACTION_INTENT_BASE_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b[^.!?\n]{0,40}\b(?:now|for you|in this response)\b`, 'i'),
|
|
@@ -204,6 +206,14 @@ const DISCORD_ACTION_INTENT_PATTERNS = [
|
|
|
204
206
|
/\b(?:(?:i am|i'm)\s+)?already handling (?:it|that|this)(?: now)?\b/i,
|
|
205
207
|
/\b(?:(?:i am|i'm)\s+)?taking the next pass(?: now)?\b/i,
|
|
206
208
|
/\b(?:(?:i am|i'm)\s+)?cleaning(?: [^.!?\n]{0,40})? up now\b/i,
|
|
209
|
+
// Headless past — sentence-initial past verb + resource noun, no subject.
|
|
210
|
+
new RegExp(String.raw `^(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
|
|
211
|
+
// First-person past — I + past verb + resource noun.
|
|
212
|
+
new RegExp(String.raw `\bI\s+(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
|
|
213
|
+
// Perfect tense — I've / I have + past participle + resource noun.
|
|
214
|
+
new RegExp(String.raw `\b(?:I've|I have)\s+(?:${DISCORD_ACTION_INTENT_PAST_PARTICIPLES})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
|
|
215
|
+
// Done-prefix — "Done" at sentence start followed by past verb + resource noun.
|
|
216
|
+
new RegExp(String.raw `^Done\b[^a-zA-Z]{0,10}(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
|
|
207
217
|
];
|
|
208
218
|
function stripCodeLikeDiscordActionText(text) {
|
|
209
219
|
return text
|
|
@@ -238,7 +248,7 @@ export function buildPromisedDiscordActionWithoutExecutionNotice(visibleReplyTex
|
|
|
238
248
|
return '';
|
|
239
249
|
if (!claimsImmediateDiscordActionIntent(visibleReplyText))
|
|
240
250
|
return '';
|
|
241
|
-
return 'Warning: this reply says Discord-managed work
|
|
251
|
+
return 'Warning: this reply says Discord-managed work was performed or is being performed, but this turn ended with zero actionable `<discord-action>` blocks and zero executed action results. If work has not started yet, say that clearly instead.';
|
|
242
252
|
}
|
|
243
253
|
export function appendPromisedDiscordActionWithoutExecutionNotice(text, actionsCount, actionResultsCount) {
|
|
244
254
|
const notice = buildPromisedDiscordActionWithoutExecutionNotice(text, actionsCount, actionResultsCount);
|
|
@@ -172,6 +172,36 @@ describe('claimsImmediateDiscordActionIntent', () => {
|
|
|
172
172
|
expect(claimsImmediateDiscordActionIntent("I'm reading that now.")).toBe(false);
|
|
173
173
|
expect(claimsImmediateDiscordActionIntent("I'm listing that now.")).toBe(false);
|
|
174
174
|
});
|
|
175
|
+
it('matches headless past-tense claims (no subject)', () => {
|
|
176
|
+
expect(claimsImmediateDiscordActionIntent('Posted the plan to the channel')).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
it('matches first-person past-tense claims', () => {
|
|
179
|
+
expect(claimsImmediateDiscordActionIntent('I posted the SEO plan to #website')).toBe(true);
|
|
180
|
+
expect(claimsImmediateDiscordActionIntent('I sent the message')).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
it('matches perfect-tense claims', () => {
|
|
183
|
+
expect(claimsImmediateDiscordActionIntent("I've posted it to the channel")).toBe(true);
|
|
184
|
+
expect(claimsImmediateDiscordActionIntent('I have created the task')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('matches done-prefix past-tense claims', () => {
|
|
187
|
+
expect(claimsImmediateDiscordActionIntent('Done. Sent the message.')).toBe(true);
|
|
188
|
+
expect(claimsImmediateDiscordActionIntent('Done — posted the reply')).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it('does not match past-tense without a resource noun', () => {
|
|
191
|
+
expect(claimsImmediateDiscordActionIntent('I posted about this on my blog')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
it('does not match third-person past-tense (no "I" subject)', () => {
|
|
194
|
+
expect(claimsImmediateDiscordActionIntent('The user posted a message earlier')).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
it('does not match conditional past-tense', () => {
|
|
197
|
+
expect(claimsImmediateDiscordActionIntent('If I posted the task, it would appear in the thread')).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
it('does not match capability phrasing with past-tense resource nouns (regression)', () => {
|
|
200
|
+
expect(claimsImmediateDiscordActionIntent("I can post the message when you're ready")).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
it('does not match example-prefixed past-tense claims', () => {
|
|
203
|
+
expect(claimsImmediateDiscordActionIntent('Example: I posted the reply')).toBe(false);
|
|
204
|
+
});
|
|
175
205
|
});
|
|
176
206
|
describe('buildPromisedDiscordActionWithoutExecutionNotice', () => {
|
|
177
207
|
it('returns empty string when the reply does not claim immediate action intent', () => {
|
|
@@ -183,18 +213,24 @@ describe('buildPromisedDiscordActionWithoutExecutionNotice', () => {
|
|
|
183
213
|
});
|
|
184
214
|
it('returns a warning when the reply promises current work but nothing ran', () => {
|
|
185
215
|
const out = buildPromisedDiscordActionWithoutExecutionNotice("I'm creating that task now.", 0, 0);
|
|
186
|
-
expect(out).toContain('Discord-managed work
|
|
216
|
+
expect(out).toContain('Discord-managed work was performed or is being performed');
|
|
187
217
|
expect(out).toContain('zero actionable `<discord-action>` blocks');
|
|
188
218
|
expect(out).toContain('zero executed action results');
|
|
189
219
|
});
|
|
190
220
|
it('returns a warning for progress phrases the prompt guidance already forbids without actions', () => {
|
|
191
221
|
const out = buildPromisedDiscordActionWithoutExecutionNotice('Taking the next pass now.', 0, 0);
|
|
192
|
-
expect(out).toContain('Discord-managed work
|
|
222
|
+
expect(out).toContain('Discord-managed work was performed or is being performed');
|
|
193
223
|
});
|
|
194
224
|
it('returns empty string for generic assistant prose without Discord-action intent', () => {
|
|
195
225
|
expect(buildPromisedDiscordActionWithoutExecutionNotice('Let me check that now.', 0, 0)).toBe('');
|
|
196
226
|
expect(buildPromisedDiscordActionWithoutExecutionNotice("I'll show you that now.", 0, 0)).toBe('');
|
|
197
227
|
});
|
|
228
|
+
it('returns a warning for a past-tense claim with zero actions/results', () => {
|
|
229
|
+
const out = buildPromisedDiscordActionWithoutExecutionNotice('I posted the plan to the channel.', 0, 0);
|
|
230
|
+
expect(out).toContain('Discord-managed work was performed or is being performed');
|
|
231
|
+
expect(out).toContain('zero actionable `<discord-action>` blocks');
|
|
232
|
+
expect(out).toContain('zero executed action results');
|
|
233
|
+
});
|
|
198
234
|
});
|
|
199
235
|
describe('appendPromisedDiscordActionWithoutExecutionNotice', () => {
|
|
200
236
|
it('appends the warning beneath the visible reply text', () => {
|
package/dist/index.js
CHANGED
|
@@ -425,6 +425,7 @@ const summaryMaxChars = cfg.summaryMaxChars;
|
|
|
425
425
|
const summaryEveryNTurns = cfg.summaryEveryNTurns;
|
|
426
426
|
const summaryMaxTokens = cfg.summaryMaxTokens;
|
|
427
427
|
const summaryTargetRatio = cfg.summaryTargetRatio;
|
|
428
|
+
const capsuleTtlMs = cfg.capsuleTtlMs;
|
|
428
429
|
const summaryDataDir = cfg.summaryDataDirOverride
|
|
429
430
|
|| (dataDir ? path.join(dataDir, 'memory', 'rolling') : path.join(__dirname, '..', 'data', 'memory', 'rolling'));
|
|
430
431
|
const summaryArchiveDir = cfg.summaryArchiveDirOverride
|
|
@@ -1275,6 +1276,7 @@ const botParams = {
|
|
|
1275
1276
|
summaryTargetRatio,
|
|
1276
1277
|
summaryDataDir,
|
|
1277
1278
|
summaryArchiveDir,
|
|
1279
|
+
capsuleTtlMs,
|
|
1278
1280
|
durableMemoryEnabled,
|
|
1279
1281
|
durableDataDir,
|
|
1280
1282
|
durableInjectMaxChars,
|
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discoclaw",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Personal AI orchestrator that turns Discord into a persistent workspace",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"discord",
|
|
8
|
+
"bot",
|
|
9
|
+
"ai",
|
|
10
|
+
"assistant",
|
|
11
|
+
"orchestrator",
|
|
12
|
+
"automation",
|
|
13
|
+
"claude",
|
|
14
|
+
"openai",
|
|
15
|
+
"memory",
|
|
16
|
+
"tasks",
|
|
17
|
+
"voice",
|
|
18
|
+
"personal-ai"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://discoclaw.ai",
|
|
6
21
|
"repository": {
|
|
7
22
|
"type": "git",
|
|
8
23
|
"url": "https://github.com/DiscoClaw/discoclaw.git"
|