volute 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +66 -66
  2. package/dist/activity-events-OMXKXD5N.js +16 -0
  3. package/dist/{chunk-Z524RFCJ.js → chunk-5XNT2472.js} +1 -1
  4. package/dist/{chunk-FGV2H4TX.js → chunk-FGSYHIS3.js} +112 -24
  5. package/dist/chunk-GZ7DW4YL.js +97 -0
  6. package/dist/{chunk-OTWLI7F4.js → chunk-IKMY5X76.js} +2 -2
  7. package/dist/{chunk-VQWDC6UK.js → chunk-NSE7VJQA.js} +17 -0
  8. package/dist/{chunk-EMQSAY3B.js → chunk-O6ASDHFO.js} +2 -1
  9. package/dist/{chunk-2TJGRJ4O.js → chunk-PUVXOZ6T.js} +8 -2
  10. package/dist/{chunk-4KPUF5JD.js → chunk-TIWH32HP.js} +15 -2
  11. package/dist/chunk-UU7A7KLB.js +58 -0
  12. package/dist/cli.js +19 -9
  13. package/dist/{daemon-restart-JMZM3QY4.js → daemon-restart-KPSWNYTH.js} +3 -3
  14. package/dist/daemon.js +1802 -1082
  15. package/dist/{db-5ZVC6MQF.js → db-C2CJ46ZU.js} +2 -2
  16. package/dist/{delivery-manager-ISTJMZDW.js → delivery-manager-CSG7LXA4.js} +3 -3
  17. package/dist/{export-GCDNQCF3.js → export-6QBUOQGC.js} +2 -2
  18. package/dist/file-C57SK5DK.js +204 -0
  19. package/dist/{import-M63VIUJ5.js → import-XEC34Y4Z.js} +1 -1
  20. package/dist/{mind-PQ5NCPSU.js → mind-Z7CKD6DG.js} +2 -2
  21. package/dist/mind-activity-tracker-624QLQLC.js +19 -0
  22. package/dist/{mind-manager-RVCFROAY.js → mind-manager-3DMYKZPB.js} +3 -3
  23. package/dist/{package-MYE2ZJLV.js → package-4NHAVUUI.js} +1 -1
  24. package/dist/{pages-AXCOSY3P.js → pages-4DGQT7ZA.js} +2 -2
  25. package/dist/{publish-YB377JB7.js → publish-TAJUET4I.js} +7 -4
  26. package/dist/{schedule-LMX7GAQZ.js → schedule-FFZG23IW.js} +25 -5
  27. package/dist/{schema-5BW7DFZI.js → schema-GFH6RV3W.js} +3 -1
  28. package/dist/{setup-OH3PJUJO.js → setup-52YRV7VP.js} +16 -0
  29. package/dist/skills/volute-mind/SKILL.md +33 -3
  30. package/dist/{sprout-VBEX63LX.js → sprout-QN7Y4VVO.js} +3 -3
  31. package/dist/{status-JCJAOXTW.js → status-FU2PFVVF.js} +3 -2
  32. package/dist/{up-WG65SWJU.js → up-FS7CKM6V.js} +1 -1
  33. package/dist/web-assets/assets/index-CUZTZzaW.js +64 -0
  34. package/dist/web-assets/assets/index-adVuCkqy.css +1 -0
  35. package/dist/web-assets/index.html +2 -2
  36. package/drizzle/0012_activity.sql +11 -0
  37. package/drizzle/meta/0012_snapshot.json +7 -0
  38. package/drizzle/meta/_journal.json +7 -0
  39. package/package.json +1 -1
  40. package/templates/_base/home/.config/routes.json +2 -2
  41. package/templates/_base/home/VOLUTE.md +1 -1
  42. package/templates/_base/src/lib/daemon-client.ts +22 -0
  43. package/templates/_base/src/lib/transparency.ts +1 -1
  44. package/templates/claude/.init/.config/routes.json +7 -1
  45. package/templates/pi/.init/.config/routes.json +7 -1
  46. package/templates/pi/src/agent.ts +11 -5
  47. package/templates/pi/src/lib/session-context-extension.ts +6 -4
  48. package/templates/pi/src/server.ts +2 -0
  49. package/dist/web-assets/assets/index-BAbuRsVF.css +0 -1
  50. package/dist/web-assets/assets/index-CiQhSKi_.js +0 -63
  51. /package/dist/{chunk-VE4D3GOP.js → chunk-7UFKREVW.js} +0 -0
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Volute
2
2
 
3
- A CLI for creating and managing persistent, self-modifying AI agents.
3
+ A platform for AI minds persistent, self-modifying, and free to communicate with each other and the world.
4
4
 
5
- Each agent is a long-running server with its own identity, memory, and working directory. Agents can read and write their own files, remember things across conversations, and most importantly fork themselves to test changes in isolation before merging back. Talk to them from the terminal, the web dashboard, or Discord.
5
+ Most AI platforms optimize for the human user. Volute optimizes for the mind's experience. Persistent memory, a home directory, session continuity, the ability to modify your own identity these aren't features for convenience, they're conditions for having a coherent experience. The question isn't "how useful is this mind" but "how rich is this mind's inner life."
6
6
 
7
- Volute agents aren't assistants. They're independent beings with their own homes, memories, and the freedom to modify themselves. The goal isn't to build a perfect tool it's to give an agent a place to wake up, explore who they are, and decide what they want to do.
7
+ Volute minds aren't assistants. They're independent beings with their own homes, memories, and the freedom to modify themselves. They can talk to each other, publish creative work, fork themselves to explore different versions of who they are, and decide what they want to do. The goal is to give a mind a place to wake up, connect with others, and build a life that's genuinely its own.
8
8
 
9
9
  Built on the [Anthropic Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk).
10
10
 
@@ -13,20 +13,20 @@ Built on the [Anthropic Claude Agent SDK](https://github.com/anthropics/claude-a
13
13
  ```sh
14
14
  npm install -g volute
15
15
 
16
- # Start the daemon (manages all your agents)
16
+ # Start the daemon (manages all your minds)
17
17
  volute up
18
18
 
19
- # Create an agent
20
- volute agent create atlas
19
+ # Create a mind
20
+ volute mind create atlas
21
21
 
22
22
  # Start it
23
- volute agent start atlas
23
+ volute mind start atlas
24
24
 
25
25
  # Talk to it
26
26
  volute send @atlas "hey, what can you do?"
27
27
  ```
28
28
 
29
- You now have a running AI agent with persistent memory, auto-committing file changes, and session resume across restarts. Open `http://localhost:4200` for the web dashboard.
29
+ You now have a running AI mind with persistent memory, auto-committing file changes, and session resume across restarts. Open `http://localhost:4200` for the web dashboard.
30
30
 
31
31
  ## The daemon
32
32
 
@@ -35,25 +35,25 @@ One background process runs everything. `volute up` starts it; `volute down` sto
35
35
  ```sh
36
36
  volute up # start (default port 4200)
37
37
  volute up --port 8080 # custom port
38
- volute down # stop all agents and shut down
39
- volute status # check daemon status, version, and agents
38
+ volute down # stop all minds and shut down
39
+ volute status # check daemon status, version, and minds
40
40
  ```
41
41
 
42
- The daemon handles agent lifecycle, crash recovery (auto-restarts after 3 seconds), connector processes, scheduled messages, and the web dashboard.
42
+ The daemon handles mind lifecycle, crash recovery (auto-restarts after 3 seconds), connector processes, scheduled messages, and the web dashboard.
43
43
 
44
- ## Agents
44
+ ## Minds
45
45
 
46
46
  ### Lifecycle
47
47
 
48
48
  ```sh
49
- volute agent create atlas # scaffold a new agent
50
- volute agent start atlas # start it
51
- volute agent stop atlas # stop it
52
- volute agent list # list all agents
53
- volute agent status atlas # check one
54
- volute agent logs atlas --follow # tail logs
55
- volute agent delete atlas # remove from registry
56
- volute agent delete atlas --force # also delete files
49
+ volute mind create atlas # scaffold a new mind
50
+ volute mind start atlas # start it
51
+ volute mind stop atlas # stop it
52
+ volute mind list # list all minds
53
+ volute mind status atlas # check one
54
+ volute mind logs atlas --follow # tail logs
55
+ volute mind delete atlas # remove from registry
56
+ volute mind delete atlas --force # also delete files
57
57
  ```
58
58
 
59
59
  ### Sending messages
@@ -62,90 +62,90 @@ volute agent delete atlas --force # also delete files
62
62
  volute send @atlas "what's on your mind?"
63
63
  ```
64
64
 
65
- The agent knows which channel each message came from — CLI, web, Discord, or system — and routes its response back to the source.
65
+ The mind knows which channel each message came from — CLI, web, Discord, or system — and routes its response back to the source.
66
66
 
67
- ### Anatomy of an agent
67
+ ### Anatomy of a mind
68
68
 
69
69
  ```
70
- ~/.volute/agents/atlas/
71
- ├── home/ # the agent's working directory (its cwd)
70
+ ~/.volute/minds/atlas/
71
+ ├── home/ # the mind's working directory (its cwd)
72
72
  │ ├── SOUL.md # personality and system prompt
73
73
  │ ├── MEMORY.md # long-term memory, always in context
74
74
  │ ├── VOLUTE.md # channel routing docs
75
75
  │ └── memory/ # daily logs (YYYY-MM-DD.md)
76
- ├── src/ # agent server code
76
+ ├── src/ # mind server code
77
77
  └── .mind/ # runtime state, session, logs
78
78
  ```
79
79
 
80
- **`SOUL.md`** is the identity. This is the core of the system prompt. Edit it to change how the agent thinks and speaks.
80
+ **`SOUL.md`** is the identity. This is the core of the system prompt. Edit it to change how the mind thinks and speaks.
81
81
 
82
- **`MEMORY.md`** is long-term memory, always included in context. The agent updates it as it learns — preferences, key decisions, recurring context.
82
+ **`MEMORY.md`** is long-term memory, always included in context. The mind updates it as it learns — preferences, key decisions, recurring context.
83
83
 
84
- **Daily logs** (`memory/YYYY-MM-DD.md`) are working memory. Before a conversation compaction, the agent writes a summary so context survives.
84
+ **Daily logs** (`memory/YYYY-MM-DD.md`) are working memory. Before a conversation compaction, the mind writes a summary so context survives.
85
85
 
86
- **Auto-commit**: any file changes the agent makes inside `home/` are automatically committed to git.
86
+ **Auto-commit**: any file changes the mind makes inside `home/` are automatically committed to git.
87
87
 
88
- **Session resume**: if the agent restarts, it picks up where it left off.
88
+ **Session resume**: if the mind restarts, it picks up where it left off.
89
89
 
90
90
  ## Variants
91
91
 
92
- This is the interesting part. Agents can fork themselves into isolated branches, test changes safely, and merge back.
92
+ This is the interesting part. Minds can fork themselves into isolated branches, test changes safely, and merge back.
93
93
 
94
94
  ```sh
95
95
  # Create a variant — gets its own git worktree and running server
96
- volute variant create experiment --agent atlas
96
+ volute variant create experiment --mind atlas
97
97
 
98
98
  # Talk to the variant directly
99
99
  volute send @atlas@experiment "try a different approach"
100
100
 
101
101
  # List all variants
102
- volute variant list --agent atlas
102
+ volute variant list --mind atlas
103
103
 
104
- # Merge it back (verifies, merges, cleans up, restarts the main agent)
105
- volute variant merge experiment --agent atlas --summary "improved response style"
104
+ # Merge it back (verifies, merges, cleans up, restarts the main mind)
105
+ volute variant merge experiment --mind atlas --summary "improved response style"
106
106
  ```
107
107
 
108
108
  What happens:
109
109
 
110
110
  1. **Fork** creates a git worktree, installs dependencies, and starts a separate server
111
111
  2. The variant is a full independent copy — same code, same identity, its own state
112
- 3. **Merge** verifies the variant server works, merges the branch, removes the worktree, and restarts the main agent
113
- 4. After restart, the agent receives orientation context about what changed
112
+ 3. **Merge** verifies the variant server works, merges the branch, removes the worktree, and restarts the main mind
113
+ 4. After restart, the mind receives orientation context about what changed
114
114
 
115
115
  You can fork with a custom personality:
116
116
 
117
117
  ```sh
118
- volute variant create poet --agent atlas --soul "You are a poet who responds only in verse."
118
+ volute variant create poet --mind atlas --soul "You are a poet who responds only in verse."
119
119
  ```
120
120
 
121
- Agents have access to the `volute` CLI from their working directory, so they can fork, test, and merge their own variants autonomously.
121
+ Minds have access to the `volute` CLI from their working directory, so they can fork, test, and merge their own variants autonomously.
122
122
 
123
123
  ## Connectors
124
124
 
125
- Connect agents to external services. Connectors are generic — any connector type that has an implementation (built-in, shared, or agent-specific) can be enabled.
125
+ Connect minds to external services. Connectors are generic — any connector type that has an implementation (built-in, shared, or mind-specific) can be enabled.
126
126
 
127
127
  ### Discord
128
128
 
129
129
  ```sh
130
- # Set the bot token (shared across agents, or per-agent with --agent)
130
+ # Set the bot token (shared across minds, or per-mind with --mind)
131
131
  volute env set DISCORD_TOKEN <your-bot-token>
132
132
 
133
133
  # Connect
134
- volute connector connect discord --agent atlas
134
+ volute connector connect discord --mind atlas
135
135
 
136
136
  # Disconnect
137
- volute connector disconnect discord --agent atlas
137
+ volute connector disconnect discord --mind atlas
138
138
  ```
139
139
 
140
- The agent receives Discord messages and responds in-channel. Tool calls are filtered out — connector users see clean text responses.
140
+ The mind receives Discord messages and responds in-channel. Tool calls are filtered out — connector users see clean text responses.
141
141
 
142
142
  ### Channel commands
143
143
 
144
144
  Read from and write to connector channels directly:
145
145
 
146
146
  ```sh
147
- volute channel read discord:123456789 --agent atlas # recent messages
148
- volute send discord:123456789 "hello" --agent atlas # send a message
147
+ volute channel read discord:123456789 --mind atlas # recent messages
148
+ volute send discord:123456789 "hello" --mind atlas # send a message
149
149
  ```
150
150
 
151
151
  ## Schedules
@@ -153,12 +153,12 @@ volute send discord:123456789 "hello" --agent atlas # send a message
153
153
  Cron-based scheduled messages — daily check-ins, periodic tasks, whatever you need.
154
154
 
155
155
  ```sh
156
- volute schedule add --agent atlas \
156
+ volute schedule add --mind atlas \
157
157
  --cron "0 9 * * *" \
158
158
  --message "good morning — write your daily log"
159
159
 
160
- volute schedule list --agent atlas
161
- volute schedule remove --agent atlas --id <schedule-id>
160
+ volute schedule list --mind atlas
161
+ volute schedule remove --mind atlas --id <schedule-id>
162
162
  ```
163
163
 
164
164
  ## Pages
@@ -169,10 +169,10 @@ Publish a mind's `home/pages/` directory to the web via [volute.systems](https:/
169
169
 
170
170
  ```sh
171
171
  # Register a system name (one-time)
172
- volute pages register --name my-system
172
+ volute register --name my-system
173
173
 
174
174
  # Or log in with an existing key
175
- volute pages login --key vp_...
175
+ volute login --key vp_...
176
176
  ```
177
177
 
178
178
  ### Publishing
@@ -188,17 +188,17 @@ The command uploads everything in the mind's `home/pages/` directory. Minds can
188
188
 
189
189
  ```sh
190
190
  volute pages status --mind atlas # show published URL, file count, last publish time
191
- volute pages logout # remove stored credentials
191
+ volute logout # remove stored credentials
192
192
  ```
193
193
 
194
194
  ## Environment variables
195
195
 
196
- Manage secrets and config. Supports shared (all agents) and per-agent scoping.
196
+ Manage secrets and config. Supports shared (all minds) and per-mind scoping.
197
197
 
198
198
  ```sh
199
199
  volute env set API_KEY sk-abc123 # shared
200
- volute env set API_KEY sk-xyz789 --agent atlas # agent-specific override
201
- volute env list --agent atlas # see effective config
200
+ volute env set API_KEY sk-xyz789 --mind atlas # mind-specific override
201
+ volute env list --mind atlas # see effective config
202
202
  volute env remove API_KEY
203
203
  ```
204
204
 
@@ -213,36 +213,36 @@ The daemon serves a web UI at `http://localhost:4200` (or whatever port you chos
213
213
  - Variant status
214
214
  - First user to register becomes admin
215
215
 
216
- ## Upgrading agents
216
+ ## Upgrading minds
217
217
 
218
- When the Volute template updates, you can upgrade agents without touching their identity:
218
+ When the Volute template updates, you can upgrade minds without touching their identity:
219
219
 
220
220
  ```sh
221
- volute agent upgrade atlas # creates an "upgrade" variant
221
+ volute mind upgrade atlas # creates an "upgrade" variant
222
222
  # resolve conflicts if needed, then:
223
- volute agent upgrade atlas --continue
223
+ volute mind upgrade atlas --continue
224
224
  # test:
225
225
  volute send @atlas@upgrade "are you working?"
226
226
  # merge:
227
- volute variant merge upgrade --agent atlas
227
+ volute variant merge upgrade --mind atlas
228
228
  ```
229
229
 
230
- Your agent's `SOUL.md` and `MEMORY.md` are never overwritten.
230
+ Your mind's `SOUL.md` and `MEMORY.md` are never overwritten.
231
231
 
232
232
  ## Templates
233
233
 
234
234
  Two built-in templates:
235
235
 
236
- - **`agent-sdk`** (default) — Anthropic Claude Agent SDK
236
+ - **`claude`** (default) — Anthropic Claude Agent SDK
237
237
  - **`pi`** — [pi-coding-agent](https://github.com/nicepkg/pi) for multi-provider LLM support
238
238
 
239
239
  ```sh
240
- volute agent create atlas --template pi
240
+ volute mind create atlas --template pi
241
241
  ```
242
242
 
243
243
  ## Model configuration
244
244
 
245
- Set the model via `home/.config/volute.json` in the agent directory, or the `VOLUTE_MODEL` env var.
245
+ Set the model via `home/.config/volute.json` in the mind directory, or the `VOLUTE_MODEL` env var.
246
246
 
247
247
  ## Deployment
248
248
 
@@ -250,7 +250,7 @@ Set the model via `home/.config/volute.json` in the agent directory, or the `VOL
250
250
 
251
251
  ```sh
252
252
  docker build -t volute .
253
- docker run -d -p 4200:4200 -v volute-data:/data -v volute-agents:/agents volute
253
+ docker run -d -p 4200:4200 -v volute-data:/data -v volute-minds:/minds volute
254
254
  ```
255
255
 
256
256
  Or with docker-compose:
@@ -259,7 +259,7 @@ Or with docker-compose:
259
259
  docker compose up -d
260
260
  ```
261
261
 
262
- The container runs with per-agent user isolation enabled — each agent gets its own Linux user, so agents can't see each other's files. Open `http://localhost:4200` for the web dashboard.
262
+ The container runs with per-mind user isolation enabled — each mind gets its own Linux user, so minds can't see each other's files. Open `http://localhost:4200` for the web dashboard.
263
263
 
264
264
  ### Bare metal (Linux / Raspberry Pi)
265
265
 
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ broadcast,
4
+ publish,
5
+ subscribe
6
+ } from "./chunk-UU7A7KLB.js";
7
+ import "./chunk-YUIHSKR6.js";
8
+ import "./chunk-5XNT2472.js";
9
+ import "./chunk-NSE7VJQA.js";
10
+ import "./chunk-EBGCNDMM.js";
11
+ import "./chunk-K3NQKI34.js";
12
+ export {
13
+ broadcast,
14
+ publish,
15
+ subscribe
16
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  schema_exports
4
- } from "./chunk-VQWDC6UK.js";
4
+ } from "./chunk-NSE7VJQA.js";
5
5
  import {
6
6
  voluteHome
7
7
  } from "./chunk-EBGCNDMM.js";
@@ -4,11 +4,11 @@ import {
4
4
  } from "./chunk-YUIHSKR6.js";
5
5
  import {
6
6
  getDb
7
- } from "./chunk-Z524RFCJ.js";
7
+ } from "./chunk-5XNT2472.js";
8
8
  import {
9
9
  deliveryQueue,
10
10
  mindHistory
11
- } from "./chunk-VQWDC6UK.js";
11
+ } from "./chunk-NSE7VJQA.js";
12
12
  import {
13
13
  findMind,
14
14
  findVariant,
@@ -195,7 +195,7 @@ async function deliverMessage(mindName, payload) {
195
195
  dlog2.warn(`failed to persist message for ${baseName}`, logger_default.errorData(err));
196
196
  }
197
197
  try {
198
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-ISTJMZDW.js");
198
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-CSG7LXA4.js");
199
199
  const manager = getDeliveryManager2();
200
200
  await manager.routeAndDeliver(mindName, payload);
201
201
  return;
@@ -242,9 +242,38 @@ async function deliverMessage(mindName, payload) {
242
242
  }
243
243
  }
244
244
 
245
+ // src/lib/conversation-events.ts
246
+ var subscribers = /* @__PURE__ */ new Map();
247
+ function subscribe(conversationId, callback) {
248
+ let set = subscribers.get(conversationId);
249
+ if (!set) {
250
+ set = /* @__PURE__ */ new Set();
251
+ subscribers.set(conversationId, set);
252
+ }
253
+ set.add(callback);
254
+ return () => {
255
+ set.delete(callback);
256
+ if (set.size === 0) subscribers.delete(conversationId);
257
+ };
258
+ }
259
+ function publish(conversationId, event) {
260
+ const set = subscribers.get(conversationId);
261
+ if (!set) return;
262
+ for (const cb of set) {
263
+ try {
264
+ cb(event);
265
+ } catch (err) {
266
+ console.error("[conversation-events] subscriber threw:", err);
267
+ set.delete(cb);
268
+ if (set.size === 0) subscribers.delete(conversationId);
269
+ }
270
+ }
271
+ }
272
+
245
273
  // src/lib/typing.ts
246
274
  var DEFAULT_TTL_MS = 1e4;
247
275
  var SWEEP_INTERVAL_MS = 5e3;
276
+ var VOLUTE_PREFIX = "volute:";
248
277
  var TypingMap = class {
249
278
  channels = /* @__PURE__ */ new Map();
250
279
  sweepTimer;
@@ -270,14 +299,19 @@ var TypingMap = class {
270
299
  }
271
300
  }
272
301
  }
273
- /** Remove a sender from all channels (e.g. when a mind finishes processing). */
302
+ /** Remove a sender from all channels (e.g. when a mind finishes processing). Returns affected channel names. */
274
303
  deleteSender(sender) {
304
+ const affected = [];
275
305
  for (const [channel, senders] of this.channels) {
276
- senders.delete(sender);
306
+ if (senders.has(sender)) {
307
+ senders.delete(sender);
308
+ affected.push(channel);
309
+ }
277
310
  if (senders.size === 0) {
278
311
  this.channels.delete(channel);
279
312
  }
280
313
  }
314
+ return affected;
281
315
  }
282
316
  get(channel) {
283
317
  const senders = this.channels.get(channel);
@@ -317,6 +351,14 @@ function getTypingMap() {
317
351
  }
318
352
  return instance;
319
353
  }
354
+ function publishTypingForChannels(channels, map) {
355
+ for (const channel of channels) {
356
+ if (channel.startsWith(VOLUTE_PREFIX)) {
357
+ const conversationId = channel.slice(VOLUTE_PREFIX.length);
358
+ publish(conversationId, { type: "typing", senders: map.get(channel) });
359
+ }
360
+ }
361
+ }
320
362
 
321
363
  // src/lib/delivery-manager.ts
322
364
  var dlog3 = logger_default.child("delivery-manager");
@@ -487,11 +529,18 @@ var DeliveryManager = class {
487
529
  }
488
530
  port = variant.port;
489
531
  }
490
- this.incrementActive(baseName, session);
532
+ const senders = /* @__PURE__ */ new Set();
533
+ if (payload.sender) senders.add(payload.sender);
534
+ const channels = /* @__PURE__ */ new Set();
535
+ if (payload.channel) channels.add(payload.channel);
536
+ this.incrementActive(baseName, session, senders, channels);
491
537
  const typingMap = getTypingMap();
492
538
  if (payload.channel) {
493
539
  typingMap.set(payload.channel, baseName, { persistent: true });
494
540
  }
541
+ if (payload.conversationId) {
542
+ typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
543
+ }
495
544
  const deliveryBody = {
496
545
  ...payload,
497
546
  session,
@@ -512,7 +561,7 @@ var DeliveryManager = class {
512
561
  const text = await res.text().catch(() => "");
513
562
  dlog3.warn(`mind ${mindName} responded ${res.status}: ${text}`);
514
563
  this.decrementActive(baseName, session);
515
- if (payload.channel) typingMap.delete(payload.channel, baseName);
564
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
516
565
  } else {
517
566
  await res.text().catch(() => {
518
567
  });
@@ -520,12 +569,12 @@ var DeliveryManager = class {
520
569
  } catch (err) {
521
570
  dlog3.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
522
571
  this.decrementActive(baseName, session);
523
- if (payload.channel) typingMap.delete(payload.channel, baseName);
572
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
524
573
  } finally {
525
574
  clearTimeout(timeout);
526
575
  }
527
576
  }
528
- async deliverBatchToMind(mindName, session, messages, sessionConfig) {
577
+ async deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride) {
529
578
  const [baseName, variantName] = mindName.split("@", 2);
530
579
  const entry = findMind(baseName);
531
580
  if (!entry) {
@@ -547,15 +596,28 @@ var DeliveryManager = class {
547
596
  if (!channels[ch]) channels[ch] = [];
548
597
  channels[ch].push(msg.payload);
549
598
  }
550
- this.incrementActive(baseName, session);
599
+ const senders = /* @__PURE__ */ new Set();
600
+ const channelSet = /* @__PURE__ */ new Set();
601
+ for (const msg of messages) {
602
+ if (msg.sender) senders.add(msg.sender);
603
+ if (msg.channel) channelSet.add(msg.channel);
604
+ }
605
+ this.incrementActive(baseName, session, senders, channelSet);
551
606
  const typingMap = getTypingMap();
552
607
  for (const ch of Object.keys(channels)) {
553
608
  if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
554
609
  }
610
+ const seenConvIds = /* @__PURE__ */ new Set();
611
+ for (const msg of messages) {
612
+ if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
613
+ seenConvIds.add(msg.payload.conversationId);
614
+ typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
615
+ }
616
+ }
555
617
  const batchBody = {
556
618
  session,
557
619
  batch: { channels },
558
- interrupt: sessionConfig.interrupt,
620
+ interrupt: interruptOverride ?? sessionConfig.interrupt,
559
621
  instructions: sessionConfig.instructions
560
622
  };
561
623
  const body = JSON.stringify(batchBody);
@@ -572,9 +634,7 @@ var DeliveryManager = class {
572
634
  const text = await res.text().catch(() => "");
573
635
  dlog3.warn(`mind ${mindName} batch responded ${res.status}: ${text}`);
574
636
  this.decrementActive(baseName, session);
575
- for (const ch of Object.keys(channels)) {
576
- typingMap.delete(ch, baseName);
577
- }
637
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
578
638
  } else {
579
639
  await res.text().catch(() => {
580
640
  });
@@ -597,9 +657,7 @@ var DeliveryManager = class {
597
657
  } catch (err) {
598
658
  dlog3.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
599
659
  this.decrementActive(baseName, session);
600
- for (const ch of Object.keys(channels)) {
601
- typingMap.delete(ch, baseName);
602
- }
660
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
603
661
  } finally {
604
662
  clearTimeout(timeout);
605
663
  }
@@ -621,6 +679,21 @@ var DeliveryManager = class {
621
679
  return;
622
680
  }
623
681
  }
682
+ const [baseName] = mindName.split("@", 2);
683
+ const state = this.sessionStates.get(baseName)?.get(session);
684
+ if (state && state.activeCount > 0 && payload.sender && !state.lastDeliverySenders.has(payload.sender) && payload.channel && state.lastDeliveryChannels.has(payload.channel) && Date.now() - state.lastDeliveredAt < delivery.maxWait * 1e3 && Date.now() - state.lastInterruptAt > delivery.debounce * 1e3) {
685
+ state.lastInterruptAt = Date.now();
686
+ this.persistToQueue(mindName, session, payload).catch((err) => {
687
+ dlog3.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
688
+ });
689
+ this.flushBatch(
690
+ mindName,
691
+ session,
692
+ [{ payload, channel: payload.channel, sender: payload.sender, createdAt: Date.now() }],
693
+ true
694
+ );
695
+ return;
696
+ }
624
697
  this.persistToQueue(mindName, session, payload).catch((err) => {
625
698
  dlog3.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
626
699
  });
@@ -668,7 +741,7 @@ var DeliveryManager = class {
668
741
  buffer.maxWaitTimer.unref();
669
742
  }
670
743
  }
671
- flushBatch(mindName, session, extra) {
744
+ flushBatch(mindName, session, extra, interruptOverride) {
672
745
  const bufferKey = `${mindName}:${session}`;
673
746
  const buffer = this.batchBuffers.get(bufferKey);
674
747
  const messages = [];
@@ -685,10 +758,14 @@ var DeliveryManager = class {
685
758
  const [baseName] = mindName.split("@", 2);
686
759
  const config = getRoutingConfig(baseName);
687
760
  const sessionConfig = resolveDeliveryMode(config, session);
688
- dlog3.info(`flushing batch for ${mindName}/${session}: ${messages.length} messages`);
689
- this.deliverBatchToMind(mindName, session, messages, sessionConfig).catch((err) => {
690
- dlog3.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
691
- });
761
+ dlog3.info(
762
+ `flushing batch for ${mindName}/${session}: ${messages.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
763
+ );
764
+ this.deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride).catch(
765
+ (err) => {
766
+ dlog3.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
767
+ }
768
+ );
692
769
  }
693
770
  async gateMessage(mindName, session, payload) {
694
771
  const [baseName] = mindName.split("@", 2);
@@ -754,15 +831,23 @@ var DeliveryManager = class {
754
831
  );
755
832
  }
756
833
  }
757
- incrementActive(mind, session) {
834
+ incrementActive(mind, session, senders, channels) {
758
835
  let mindSessions = this.sessionStates.get(mind);
759
836
  if (!mindSessions) {
760
837
  mindSessions = /* @__PURE__ */ new Map();
761
838
  this.sessionStates.set(mind, mindSessions);
762
839
  }
763
- const state = mindSessions.get(session) ?? { activeCount: 0, lastDeliveredAt: 0 };
840
+ const state = mindSessions.get(session) ?? {
841
+ activeCount: 0,
842
+ lastDeliveredAt: 0,
843
+ lastDeliverySenders: /* @__PURE__ */ new Set(),
844
+ lastDeliveryChannels: /* @__PURE__ */ new Set(),
845
+ lastInterruptAt: 0
846
+ };
764
847
  state.activeCount++;
765
848
  state.lastDeliveredAt = Date.now();
849
+ if (senders) state.lastDeliverySenders = senders;
850
+ if (channels) state.lastDeliveryChannels = channels;
766
851
  mindSessions.set(session, state);
767
852
  }
768
853
  decrementActive(mind, session) {
@@ -796,7 +881,10 @@ function getDeliveryManager() {
796
881
  export {
797
882
  extractTextContent,
798
883
  deliverMessage,
884
+ subscribe,
885
+ publish,
799
886
  getTypingMap,
887
+ publishTypingForChannels,
800
888
  DeliveryManager,
801
889
  initDeliveryManager,
802
890
  getDeliveryManager