@c4t4/heyamigo 0.9.25 → 0.10.1
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/README.md +37 -250
- package/config/memory-instructions.md +54 -5
- package/dist/config.js +23 -0
- package/dist/db/schema.js +55 -0
- package/dist/gateway/commands.js +13 -1
- package/dist/gateway/outgoing.js +12 -0
- package/dist/memory/digest-flag.js +222 -0
- package/dist/memory/journals.js +3 -24
- package/dist/memory/preamble.js +21 -0
- package/dist/memory/scheduler.js +6 -32
- package/dist/queue/thread-list.js +167 -0
- package/dist/queue/thread-weights.js +142 -0
- package/dist/queue/threads.js +271 -0
- package/dist/queue/worker.js +107 -1
- package/migrations/0010_create_threads.sql +27 -0
- package/migrations/meta/0010_snapshot.json +1134 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/dist/memory/journal-cadence.js +0 -120
- package/dist/memory/journal-nudger.js +0 -221
package/README.md
CHANGED
|
@@ -1,284 +1,71 @@
|
|
|
1
1
|
# heyamigo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A WhatsApp-resident assistant. Claude or Codex under the hood, durable SQLite queues, per-sender timezone scheduling, two-track architecture so browser work never blocks the chat.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Why this exists
|
|
14
|
-
|
|
15
|
-
Most AI tools try to be everything. heyamigo tries to be one thing well: **the AI you actually want in your group chat.**
|
|
16
|
-
|
|
17
|
-
Something simple you can talk to, ask things, share stuff with, and that gets better the more you use it.
|
|
18
|
-
|
|
19
|
-
Anthropic changed how third-party apps consume your Claude usage. Third-party tools now draw from extra usage, not your plan limits. heyamigo is **first-party**: it runs through Claude CLI, your direct Claude subscription. No middlemen, no extra costs.
|
|
20
|
-
|
|
21
|
-
---
|
|
5
|
+
```
|
|
6
|
+
WhatsApp ─► inbound ─► chat workers ─► outbound ─► WhatsApp
|
|
7
|
+
│ ▲
|
|
8
|
+
├──────► async / browser ─┤
|
|
9
|
+
└──────► memory_writes ───┘
|
|
10
|
+
```
|
|
22
11
|
|
|
23
12
|
## What it does
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
After 20 conversations it knows your partner prefers short replies, your coworker only responds to direct questions, and your friend is lactose intolerant. Facts, preferences, patterns, accumulated over time. Each person's profile grows independently, whether they talk in a group or a DM.
|
|
14
|
+
- **Long-term memory per person, per chat, per topic.** Files on disk. The agent decides what's worth keeping; background workers consolidate while you're not chatting.
|
|
15
|
+
- **A relevance watchlist.** Open loops the agent tracks on your behalf — questions you'd forget, things you're waiting on — surfaced naturally when the moment matches. Built like external working memory for the user.
|
|
16
|
+
- **Scheduling in the sender's timezone.** Natural language → `[REMIND: 2026-05-26 09:00 — ...]` or `[CRON: 0 9 * * 1 PROMPT — ...]`. Fires at the user's wall-clock 9am, not the server's. Cron variants: deliver text, run AI, kick off async work, or drive a browser.
|
|
17
|
+
- **A real Chrome.** Browser delegation via `[ASYNC-BROWSER: ...]` to a parallel Claude session on a shared logged-in Chrome over CDP. TikTok, Instagram, anywhere the owner is logged in. SSH-tunneled noVNC for setup.
|
|
18
|
+
- **Per-reply footer with confirmation tags.** Every side effect from the turn is visible: `_9.9s · 465k↑ 169↓ · +remind · +thread-new · +digest_`. No guessing whether a schedule actually got created.
|
|
19
|
+
- **Default-deny proactive messaging.** Groups stay silent unless explicitly opted in. Per-role token quotas, file-size caps, tool restrictions.
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
People, projects, topics, each in their own folder with an index.
|
|
35
|
-
|
|
36
|
-
Ask about a project and it pulls that project's brief. Ask about a person and it pulls their profile. It doesn't shove everything into one giant prompt. Only the relevant buckets load per message. The rest stays on disk, accessible when needed.
|
|
37
|
-
|
|
38
|
-
### Amigo decides what's worth remembering
|
|
39
|
-
Most bots store everything or nothing.
|
|
40
|
-
|
|
41
|
-
This one lets the AI flag moments during conversation. You mention you're moving to Berlin next month, it flags it. Your profile gets updated. You send "lol", nothing happens. The signal-to-noise ratio improves over time because the AI itself is curating what matters.
|
|
42
|
-
|
|
43
|
-
### Processes what happened while you're not chatting
|
|
44
|
-
Raw conversations sit in short-term memory. Between chats, background workers compress them into long-term profiles and topic summaries.
|
|
45
|
-
|
|
46
|
-
Like how your brain consolidates memories during sleep. The bot processes while idle, so the next conversation starts with better context than the last one ended with.
|
|
47
|
-
|
|
48
|
-
### Imports what you already have
|
|
49
|
-
Got a messy folder of notes, project docs, or an existing AI workspace? Point amigo at it. It reads through everything, distills the useful stuff, and organizes it into structured buckets (people, projects, topics). Your unstructured knowledge becomes searchable context that amigo references in every conversation. One command: `heyamigo import ~/my-notes`
|
|
50
|
-
|
|
51
|
-
### Browses the web
|
|
52
|
-
Controls a real Chrome browser. Navigates pages, takes screenshots, sends them back to WhatsApp. You can watch it browse via SSH tunnel.
|
|
53
|
-
|
|
54
|
-
---
|
|
21
|
+
For the why behind these — claim primitives, tag-as-side-effect channel, per-category learning, provider abstraction, the trade-offs that didn't survive the first revision — see [`docs/architecture.md`](docs/architecture.md).
|
|
55
22
|
|
|
56
23
|
## Quick start
|
|
57
24
|
|
|
58
|
-
### 1. Install Claude CLI and log in
|
|
59
|
-
|
|
60
25
|
```bash
|
|
61
26
|
npm install -g @anthropic-ai/claude-code
|
|
62
|
-
claude
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Run `claude` and follow the login instructions. You need an [Anthropic account](https://console.anthropic.com). After logging in, exit claude.
|
|
27
|
+
claude # log in once, then exit
|
|
66
28
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
npx @c4t4/heyamigo setup
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
That's it. The wizard handles everything:
|
|
74
|
-
- WhatsApp pairing (QR code + pairing code)
|
|
75
|
-
- Browser setup (optional)
|
|
76
|
-
- Personality selection
|
|
77
|
-
|
|
78
|
-
### 3. Start the bot
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx @c4t4/heyamigo start
|
|
29
|
+
npx @c4t4/heyamigo setup # wizard: pair WhatsApp, pick personality
|
|
30
|
+
npx @c4t4/heyamigo start # background, auto-restart
|
|
31
|
+
npx @c4t4/heyamigo logs # tail
|
|
82
32
|
```
|
|
83
33
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## Commands
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
npx @c4t4/heyamigo setup # setup wizard
|
|
92
|
-
npx @c4t4/heyamigo start # start (background, auto-restart)
|
|
93
|
-
npx @c4t4/heyamigo stop # stop
|
|
94
|
-
npx @c4t4/heyamigo restart # restart
|
|
95
|
-
npx @c4t4/heyamigo logs # tail live logs
|
|
96
|
-
npx @c4t4/heyamigo status # check if running
|
|
97
|
-
npx @c4t4/heyamigo update # update to latest version
|
|
98
|
-
npx @c4t4/heyamigo import <path> # import knowledge folder
|
|
99
|
-
npx @c4t4/heyamigo dev # foreground (development)
|
|
100
|
-
```
|
|
34
|
+
Codex instead of Claude: install `@openai/codex` and set `ai.provider: "codex"` in `config/config.json`.
|
|
101
35
|
|
|
102
|
-
|
|
36
|
+
## In-chat commands
|
|
103
37
|
|
|
104
38
|
| Command | What it does |
|
|
105
|
-
|
|
106
|
-
| `/reset` | Fresh
|
|
107
|
-
| `/status` | Session info
|
|
108
|
-
| `/
|
|
109
|
-
| `/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
## Memory
|
|
114
|
-
|
|
115
|
-
Three layers, inspired by how brains work:
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
Short-term raw messages (JSONL per chat)
|
|
119
|
-
Working memory Claude session (--resume)
|
|
120
|
-
Long-term profiles, topics, project briefs
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
storage/memory/
|
|
125
|
-
buckets/ projects, topics (imported or auto-created)
|
|
126
|
-
persons/ per-person profiles (grow over time)
|
|
127
|
-
chats/ per-chat briefs
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### How memory updates
|
|
131
|
-
|
|
132
|
-
The bot updates memory in two ways:
|
|
133
|
-
|
|
134
|
-
**Real-time (DIGEST flag):** During a conversation, Claude decides if something is worth remembering (a preference, fact, life event). It appends a hidden `[DIGEST: reason]` tag to its reply, which gets stripped before sending. This triggers a background digest within 2 minutes that updates the person's profile and the chat brief.
|
|
135
|
-
|
|
136
|
-
**Background sweep:** Every 3 hours, the bot checks all active chats for new messages that weren't flagged. This catches anything Claude missed. You can also force an immediate update with the `/digest` command in chat.
|
|
137
|
-
|
|
138
|
-
Memory is stored as plain markdown files. You can read, edit, or delete them directly.
|
|
139
|
-
|
|
140
|
-
---
|
|
39
|
+
|---|---|
|
|
40
|
+
| `/reset` | Fresh AI session for this chat |
|
|
41
|
+
| `/status` | Session info, context utilization |
|
|
42
|
+
| `/queues` | Live queue depths |
|
|
43
|
+
| `/crons` · `/reminders` | List recurring schedules + one-shots (token cost included) |
|
|
44
|
+
| `/threads` | List the relevance watchlist; resolve / drop / pause / weight |
|
|
45
|
+
| `/digest` | Force a memory consolidation now |
|
|
141
46
|
|
|
142
47
|
## Roles
|
|
143
48
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
| Role | Memory | Tools | Boundary |
|
|
147
|
-
|------|--------|-------|----------|
|
|
148
|
-
| **admin** | everything | all | unrestricted |
|
|
149
|
-
| **user** | own profile | web search | can't see other users or internals |
|
|
150
|
-
| **guest** | own profile | none | locked down, prompt-injection resistant |
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Browser
|
|
155
|
-
|
|
156
|
-
Optional. Chrome via CDP. You watch via noVNC over SSH tunnel. Setup wizard handles install.
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
You (SSH tunnel) -> noVNC -> Chrome <- Claude (CDP)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
All localhost. Nothing public.
|
|
49
|
+
`config/access.json`. Three default roles, easily extended.
|
|
163
50
|
|
|
164
|
-
|
|
51
|
+
| Role | Memory | Tools | Notes |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| admin | everything | all | unrestricted |
|
|
54
|
+
| user | own profile | web search | can't see other users or internals |
|
|
55
|
+
| guest | none | none | prompt-injection resistant |
|
|
165
56
|
|
|
166
57
|
## Personalities
|
|
167
58
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
[**Sharp**](config/personalities/sharp.md) (default)
|
|
171
|
-
Talks like a smart friend at dinner. Specific, confident, never vague. Won't hedge, won't lecture, won't sound like a brochure. Calls things out when they're obvious, meets people where they actually are. Checks every reply against: would I be embarrassed saying this out loud?
|
|
172
|
-
|
|
173
|
-
[**Casual**](config/personalities/casual.md)
|
|
174
|
-
Warm, relaxed, friend-over-coffee energy. Short messages, matches your vibe.
|
|
175
|
-
|
|
176
|
-
[**Professional**](config/personalities/professional.md)
|
|
177
|
-
Clear, efficient, business-appropriate. Gets to the answer fast.
|
|
178
|
-
|
|
179
|
-
Create your own: add a `.md` file to `config/personalities/`, point `config.json` at it.
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## Configuration
|
|
184
|
-
|
|
185
|
-
### config/config.json
|
|
186
|
-
|
|
187
|
-
Core settings. The wizard sets these up, but you can edit anytime.
|
|
188
|
-
|
|
189
|
-
```json
|
|
190
|
-
{
|
|
191
|
-
"owner": { "number": "17861234567" },
|
|
192
|
-
"triggers": { "aliases": ["heyamigo", "amigo", "claude"], "groupMode": "mention" },
|
|
193
|
-
"claude": { "model": "claude-opus-4-7", "timeoutMs": 60000 },
|
|
194
|
-
"reply": { "quoteInGroups": true, "typingIndicator": true }
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### config/access.json
|
|
199
|
-
|
|
200
|
-
Who can use the bot and what they can do. See `access.example.json` for all options.
|
|
201
|
-
|
|
202
|
-
```json
|
|
203
|
-
{
|
|
204
|
-
"users": {
|
|
205
|
-
"17861234567": { "role": "admin", "name": "Alice" },
|
|
206
|
-
"491701234567": { "role": "user", "name": "Carlos" }
|
|
207
|
-
},
|
|
208
|
-
"groups": [
|
|
209
|
-
{ "jid": "120363xxx@g.us", "name": "Family", "mode": "active", "allowedSenders": "*" },
|
|
210
|
-
{ "jid": "120363yyy@g.us", "name": "Work", "mode": "active", "allowedSenders": ["17861234567"] }
|
|
211
|
-
],
|
|
212
|
-
"dms": {
|
|
213
|
-
"defaultMode": "off",
|
|
214
|
-
"allowed": [{ "number": "491701234567", "mode": "active" }]
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Groups auto-discover with `mode: "off"` when the bot first sees a message. Flip to `"active"` to enable.
|
|
220
|
-
|
|
221
|
-
### Other files
|
|
222
|
-
|
|
223
|
-
| File | Purpose |
|
|
224
|
-
|------|---------|
|
|
225
|
-
| `config/personalities/*.md` | System prompts (sharp, casual, professional) |
|
|
226
|
-
| `.claude/settings.json` | Tool permissions for Claude CLI |
|
|
227
|
-
|
|
228
|
-
---
|
|
59
|
+
`config/personalities/*.md` — system-prompt fragments that define the bot's voice. The default (`sharp.md`) is opinionated about not people-pleasing. Swap or write your own.
|
|
229
60
|
|
|
230
61
|
## Where to run it
|
|
231
62
|
|
|
232
|
-
Needs a persistent filesystem and
|
|
233
|
-
|
|
234
|
-
| Option | Cost | Notes |
|
|
235
|
-
|--------|------|-------|
|
|
236
|
-
| **VPS** (Hetzner, DigitalOcean) | ~$5/mo | Recommended. Setup wizard just works. |
|
|
237
|
-
| **Home server / Raspberry Pi** | One-time | Always-on device at home. |
|
|
238
|
-
| **Your laptop** | Free | For testing. Bot stops when laptop sleeps. |
|
|
239
|
-
| **Cloud** (Railway, Fly) | Varies | Needs persistent volumes. No interactive setup. |
|
|
240
|
-
|
|
241
|
-
Not compatible with serverless (Lambda, Vercel). Needs a persistent WebSocket connection.
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Requirements
|
|
246
|
-
|
|
247
|
-
- Node.js 18+
|
|
248
|
-
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) (`npm install -g @anthropic-ai/claude-code`) + Anthropic account
|
|
249
|
-
- A WhatsApp account
|
|
250
|
-
- **macOS or Linux** (Windows: use WSL)
|
|
251
|
-
|
|
252
|
-
---
|
|
63
|
+
A VPS (Hetzner, DO) at ~$5/mo is the path of least resistance. Home server or Raspberry Pi also fine. Needs Node 18+, a persistent filesystem, and one outbound WebSocket to WhatsApp. Not serverless-compatible.
|
|
253
64
|
|
|
254
65
|
## Tracking memory with git
|
|
255
66
|
|
|
256
|
-
The bot
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
cd ~/heyamigo
|
|
260
|
-
git init
|
|
261
|
-
echo "storage/auth/" >> .gitignore
|
|
262
|
-
echo "storage/logs/" >> .gitignore
|
|
263
|
-
git add -A && git commit -m "initial setup"
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Never commit `storage/auth/` — it contains your WhatsApp session keys.
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Security
|
|
271
|
-
|
|
272
|
-
- `storage/auth/` contains your WhatsApp session keys. Guard them.
|
|
273
|
-
- All ports bind to localhost. Nothing exposed publicly.
|
|
274
|
-
- Baileys is an unofficial WhatsApp protocol. Use at your own risk.
|
|
275
|
-
- Role restrictions are prompt-enforced. Strong but not bulletproof.
|
|
276
|
-
- Outgoing media auto-deleted after sending.
|
|
277
|
-
|
|
278
|
-
---
|
|
67
|
+
The bot writes markdown files under `storage/memory/` as it learns. `git init` in your project root and commit periodically gives you a readable diff of what the assistant has come to believe about people and topics. Skip `storage/auth/` (WhatsApp keys) and `storage/logs/`.
|
|
279
68
|
|
|
280
69
|
## License
|
|
281
70
|
|
|
282
|
-
MIT
|
|
283
|
-
|
|
284
|
-
If you use heyamigo in your project or build something on top of it, a mention or link back is appreciated.
|
|
71
|
+
MIT. Built by [Catalin Waack](https://github.com/C4T4) · [LinkedIn](https://www.linkedin.com/in/catalinwaack/).
|
|
@@ -15,10 +15,9 @@ storage/memory/
|
|
|
15
15
|
journals/<slug>/index.md # journal spec (frontmatter+body)
|
|
16
16
|
journals/<slug>/entries.jsonl # append-only (do NOT edit)
|
|
17
17
|
journals/<slug>/observer-state.json
|
|
18
|
-
journals/<slug>/nudge-state.json
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
Relevant blocks appear in `[State]`, `[Map]`, `[Trees]`, `[Entities]`, `[Journals]` at top of each turn. Don't re-Read what's already in your preamble.
|
|
20
|
+
Relevant blocks appear in `[State]`, `[Map]`, `[Trees]`, `[Entities]`, `[Journals]`, `[Live threads]` at top of each turn. Don't re-Read what's already in your preamble.
|
|
22
21
|
|
|
23
22
|
## State + dig-deeper
|
|
24
23
|
|
|
@@ -51,11 +50,61 @@ Don't cross-log subjects (Dani's health ≠ Cata's). Ask if ambiguous.
|
|
|
51
50
|
|
|
52
51
|
### Create: `[JOURNAL-NEW:<slug> — <purpose>]`
|
|
53
52
|
|
|
54
|
-
When owner asks to track something no existing journal covers: propose purpose in one message, wait for confirmation, then emit the tag. Slug: lowercase letters/digits/hyphens, max 48 chars, starts with letter/digit. Creates `journals/<slug>/index.md` with defaults
|
|
53
|
+
When owner asks to track something no existing journal covers: propose purpose in one message, wait for confirmation, then emit the tag. Slug: lowercase letters/digits/hyphens, max 48 chars, starts with letter/digit. Creates `journals/<slug>/index.md` with defaults. Can flag first entry in same reply with a separate `[JOURNAL:<slug> — ...]` tag.
|
|
55
54
|
|
|
56
|
-
### Edit (pause/archive
|
|
55
|
+
### Edit (pause/archive)
|
|
57
56
|
|
|
58
|
-
No marker — Edit `journals/<slug>/index.md` directly. Frontmatter fields: `status` (active|paused|archived), `purpose`, `fields
|
|
57
|
+
No marker — Edit `journals/<slug>/index.md` directly. Frontmatter fields: `status` (active|paused|archived), `purpose`, `fields`. Never touch `entries.jsonl` or `observer-state.json` unless the owner asked you to fix a specific bug. Confirm the change in reply.
|
|
58
|
+
|
|
59
|
+
For recurring proactive check-ins on a journal, use a CRON (`[CRON: 0 9 * * 1 PROMPT — ...]`) or open a thread (`[THREAD-NEW: ...]`) — not journal frontmatter.
|
|
60
|
+
|
|
61
|
+
## Threads — your watchlist
|
|
62
|
+
|
|
63
|
+
A *thread* is an open loop you're tracking: a question, a waiting-on, an intent the owner mentioned in passing. Threads sit between cold memory (journals/profiles/buckets) and live conversation. You curate them.
|
|
64
|
+
|
|
65
|
+
You can see live threads for this chat in the `[Live threads]` preamble block. Bring them up naturally if relevant; don't force them. User voice always wins — if they drop a thread, learn from it.
|
|
66
|
+
|
|
67
|
+
### Lifecycle tags (all end-of-reply)
|
|
68
|
+
|
|
69
|
+
| Tag | When |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `[THREAD-NEW: title="..." summary="..." hotness=70 linked_memory=... category=...]` | open a new loop |
|
|
72
|
+
| `[THREAD-UPDATE:<id> summary="..." hotness=80]` | refine what you know |
|
|
73
|
+
| `[THREAD-TOUCH:<id>]` | you mentioned it naturally in reply |
|
|
74
|
+
| `[THREAD-COOL:<id> — wait Nd]` | not now, check back later |
|
|
75
|
+
| `[THREAD-RESOLVE:<id> — note]` | answer arrived, close it |
|
|
76
|
+
| `[THREAD-DROP:<id> — reason]` | stale, no longer relevant |
|
|
77
|
+
| `[THREAD-COMPRESS:<id> — note]` | stabilized fact — move to journal/profile via [DIGEST:] in same reply |
|
|
78
|
+
| `[THREAD-WEIGHT: <category> <0-100>]` | rare manual override on a category's default hotness |
|
|
79
|
+
|
|
80
|
+
### Rules
|
|
81
|
+
|
|
82
|
+
- Open threads sparingly. Only when the owner mentioned something with a future resolution point that they'd plausibly want to hear back on. Not for jokes, opinions, or things you'd hear about anyway.
|
|
83
|
+
- Hotness 0-100. AI-curated. Drop hotness with COOL, raise with TOUCH or UPDATE.
|
|
84
|
+
- Always RESOLVE/DROP/COMPRESS when the loop closes — don't leave stale threads accumulating.
|
|
85
|
+
- Compressing = thread stabilized into a durable fact. Pair `[THREAD-COMPRESS:<id> — ...]` with `[DIGEST: ...]` in the same reply so the fact actually gets written somewhere.
|
|
86
|
+
- One thread per loop. Don't open duplicates — check `[Live threads]` first.
|
|
87
|
+
|
|
88
|
+
### Examples
|
|
89
|
+
|
|
90
|
+
Owner says "waiting on Jana to confirm Tuesday's meeting":
|
|
91
|
+
```
|
|
92
|
+
Got it, fingers crossed she replies.
|
|
93
|
+
[THREAD-NEW: title="Jana meeting confirm" summary="owner waiting on confirmation for Tue meeting" hotness=70]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Owner mentions Jana confirmed:
|
|
97
|
+
```
|
|
98
|
+
Nice, locked in for Tuesday 10am.
|
|
99
|
+
[THREAD-RESOLVE:42 — confirmed Tue 10am]
|
|
100
|
+
[DIGEST: Jana meeting confirmed Tue 10am]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Owner brings up something unrelated, but thread #51 ("5 DMs to creators") is still hot and matches the moment:
|
|
104
|
+
```
|
|
105
|
+
quick reminder, the 5 DMs were on for today — how'd it go?
|
|
106
|
+
[THREAD-TOUCH:51]
|
|
107
|
+
```
|
|
59
108
|
|
|
60
109
|
## Two parallel tracks
|
|
61
110
|
|
package/dist/config.js
CHANGED
|
@@ -117,6 +117,29 @@ const ConfigSchema = z.object({
|
|
|
117
117
|
level: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']),
|
|
118
118
|
promptRetentionDays: z.number(),
|
|
119
119
|
}),
|
|
120
|
+
// Threads — AI-curated relevance watchlist. See src/queue/threads.ts.
|
|
121
|
+
// On by default. Reactive surface only in v1: the agent decides
|
|
122
|
+
// when to open loops, brings them up if naturally relevant, never
|
|
123
|
+
// sends unsolicited messages. To turn off, set enabled=false in
|
|
124
|
+
// config.local.json. Proactive review tick (silent-chat check-ins)
|
|
125
|
+
// is the bit that would be default-off if/when it ships.
|
|
126
|
+
threads: z
|
|
127
|
+
.object({
|
|
128
|
+
enabled: z.boolean().default(true),
|
|
129
|
+
preamblePerChat: z.number().int().positive().default(5),
|
|
130
|
+
// Soft caps used by future cleanup jobs; the worker doesn't read
|
|
131
|
+
// these yet but they're here so config.json can be authored once.
|
|
132
|
+
maxActivePerChat: z.number().int().positive().default(10),
|
|
133
|
+
hotnessCapOnCreate: z.number().int().min(0).max(100).default(70),
|
|
134
|
+
decayPerDay: z.number().int().min(0).default(2),
|
|
135
|
+
})
|
|
136
|
+
.default({
|
|
137
|
+
enabled: true,
|
|
138
|
+
preamblePerChat: 5,
|
|
139
|
+
maxActivePerChat: 10,
|
|
140
|
+
hotnessCapOnCreate: 70,
|
|
141
|
+
decayPerDay: 2,
|
|
142
|
+
}),
|
|
120
143
|
});
|
|
121
144
|
function loadJsonIfExists(path) {
|
|
122
145
|
try {
|
package/dist/db/schema.js
CHANGED
|
@@ -141,6 +141,61 @@ export const crons = sqliteTable('crons', {
|
|
|
141
141
|
.where(sql `${t.recurrence} IS NOT NULL`),
|
|
142
142
|
}));
|
|
143
143
|
// ──────────────────────────────────────────────────────────────────
|
|
144
|
+
// Threads — AI-curated relevance watchlist
|
|
145
|
+
// ──────────────────────────────────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// A "thread" is an open loop the AI is tracking on behalf of the user
|
|
148
|
+
// — the layer between cold memory (journals/profiles/buckets) and
|
|
149
|
+
// live conversation. Each row is "this is currently load-bearing for
|
|
150
|
+
// this chat." The AI promotes things here, updates hotness as signals
|
|
151
|
+
// arrive, brings them up naturally during conversation, and either
|
|
152
|
+
// resolves them (answer found), drops them (stale), or compresses
|
|
153
|
+
// them into cold memory (stabilized into a fact).
|
|
154
|
+
//
|
|
155
|
+
// Hotness (0-100) is the AI's running judgment of how relevant a
|
|
156
|
+
// thread is right now. The user's voice always wins — explicit
|
|
157
|
+
// resolves/drops + manual /threads commands override anything the
|
|
158
|
+
// AI inferred. Implicit signals (user touching the topic, ignoring
|
|
159
|
+
// surfaced threads) nudge the score by smaller amounts.
|
|
160
|
+
//
|
|
161
|
+
// Status: live (active), resolved (answer arrived), dropped (stale),
|
|
162
|
+
// compressed (moved into a journal/profile/bucket).
|
|
163
|
+
export const threads = sqliteTable('threads', {
|
|
164
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
165
|
+
targetJid: text('target_jid').notNull(), // WA JID this thread lives in
|
|
166
|
+
title: text('title').notNull(), // short label, AI-written
|
|
167
|
+
summary: text('summary').notNull(), // 1-2 line context
|
|
168
|
+
hotness: integer('hotness').notNull().default(50), // 0-100
|
|
169
|
+
status: text('status').notNull().default('live'), // live|resolved|dropped|compressed
|
|
170
|
+
linkedMemory: text('linked_memory'), // optional path to cold memory anchor
|
|
171
|
+
openedAt: integer('opened_at').notNull(),
|
|
172
|
+
lastTouchedAt: integer('last_touched_at').notNull(),
|
|
173
|
+
nextReviewAt: integer('next_review_at').notNull(),
|
|
174
|
+
resolutionNote: text('resolution_note'),
|
|
175
|
+
// Token cost attribution — when a thread review burns AI inferences
|
|
176
|
+
// (proactive tick, future feature) the cost lands here. Visible via
|
|
177
|
+
// /threads so the user can see which threads are expensive.
|
|
178
|
+
totalInputTokens: integer('total_input_tokens').notNull().default(0),
|
|
179
|
+
totalOutputTokens: integer('total_output_tokens').notNull().default(0),
|
|
180
|
+
enabled: integer('enabled').notNull().default(1),
|
|
181
|
+
createdAt: integer('created_at').notNull(),
|
|
182
|
+
}, t => ({
|
|
183
|
+
byJidHot: index('threads_by_jid_hot').on(t.targetJid, t.status, t.hotness),
|
|
184
|
+
byDue: index('threads_by_due').on(t.enabled, t.status, t.nextReviewAt),
|
|
185
|
+
}));
|
|
186
|
+
// Owner-global learned weights per category. A thread's category is
|
|
187
|
+
// derived from its linked_memory or title (e.g. "health", "jana",
|
|
188
|
+
// "work"). The weight is the AI's prior for "how much does the user
|
|
189
|
+
// care about this category" — used as the starting hotness when
|
|
190
|
+
// creating new threads in the same category, and nudged up/down by
|
|
191
|
+
// implicit signals (user engaging with / dropping threads).
|
|
192
|
+
export const threadCategoryWeights = sqliteTable('thread_category_weights', {
|
|
193
|
+
category: text('category').primaryKey(), // e.g. 'health', 'jana', 'work-dms'
|
|
194
|
+
weight: integer('weight').notNull().default(50), // 0-100
|
|
195
|
+
samples: integer('samples').notNull().default(0), // signal count, for confidence
|
|
196
|
+
updatedAt: integer('updated_at').notNull(),
|
|
197
|
+
});
|
|
198
|
+
// ──────────────────────────────────────────────────────────────────
|
|
144
199
|
// Inbound queue (Phase 4)
|
|
145
200
|
// ──────────────────────────────────────────────────────────────────
|
|
146
201
|
// Messages received on any channel, waiting to be processed by a
|
package/dist/gateway/commands.js
CHANGED
|
@@ -12,7 +12,10 @@ export async function tryCommand(ctx) {
|
|
|
12
12
|
const trimmed = ctx.text.trim();
|
|
13
13
|
if (!trimmed.startsWith(prefix))
|
|
14
14
|
return false;
|
|
15
|
-
const
|
|
15
|
+
const afterPrefix = trimmed.slice(prefix.length);
|
|
16
|
+
const tokens = afterPrefix.split(/\s+/);
|
|
17
|
+
const cmd = tokens[0]?.toLowerCase() ?? '';
|
|
18
|
+
const args = tokens.slice(1);
|
|
16
19
|
if (!cmd)
|
|
17
20
|
return false;
|
|
18
21
|
if (config.commands.reset.includes(cmd)) {
|
|
@@ -79,5 +82,14 @@ export async function tryCommand(ctx) {
|
|
|
79
82
|
await sendText(ctx.sock, ctx.jid, formatScheduleList(items, tz, onlyKind), ctx.quoted);
|
|
80
83
|
return true;
|
|
81
84
|
}
|
|
85
|
+
if (cmd === 'threads') {
|
|
86
|
+
if (!config.threads?.enabled) {
|
|
87
|
+
await sendText(ctx.sock, ctx.jid, 'threads are disabled in config. Set `threads.enabled: true` to turn on.', ctx.quoted);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const { handleThreadsCommand } = await import('../queue/thread-list.js');
|
|
91
|
+
await sendText(ctx.sock, ctx.jid, handleThreadsCommand(ctx.jid, args), ctx.quoted);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
82
94
|
return false;
|
|
83
95
|
}
|
package/dist/gateway/outgoing.js
CHANGED
|
@@ -232,6 +232,18 @@ export function formatStatsFooter(stats, extras) {
|
|
|
232
232
|
parts.push('+digest');
|
|
233
233
|
if (stats.sendTextCount > 0)
|
|
234
234
|
parts.push(plus('send-text', stats.sendTextCount));
|
|
235
|
+
// Thread watchlist — loud events only. Updates/cools are intentional
|
|
236
|
+
// no-ops in the footer (would clutter normal conversation).
|
|
237
|
+
if (stats.threadNewCount > 0)
|
|
238
|
+
parts.push(plus('thread-new', stats.threadNewCount));
|
|
239
|
+
if (stats.threadTouchCount > 0)
|
|
240
|
+
parts.push(plus('thread-touch', stats.threadTouchCount));
|
|
241
|
+
if (stats.threadResolveCount > 0)
|
|
242
|
+
parts.push(plus('thread-resolve', stats.threadResolveCount));
|
|
243
|
+
if (stats.threadDropCount > 0)
|
|
244
|
+
parts.push(plus('thread-drop', stats.threadDropCount));
|
|
245
|
+
if (stats.threadCompressCount > 0)
|
|
246
|
+
parts.push(plus('thread-compress', stats.threadCompressCount));
|
|
235
247
|
// Media — counted per-kind from the file list. e.g. +2 image, +video.
|
|
236
248
|
// 'document' shortened to 'doc' to keep the footer tight.
|
|
237
249
|
const mediaKinds = extras?.mediaKinds ?? [];
|