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.
- package/README.md +66 -66
- package/dist/activity-events-OMXKXD5N.js +16 -0
- package/dist/{chunk-Z524RFCJ.js → chunk-5XNT2472.js} +1 -1
- package/dist/{chunk-FGV2H4TX.js → chunk-FGSYHIS3.js} +112 -24
- package/dist/chunk-GZ7DW4YL.js +97 -0
- package/dist/{chunk-OTWLI7F4.js → chunk-IKMY5X76.js} +2 -2
- package/dist/{chunk-VQWDC6UK.js → chunk-NSE7VJQA.js} +17 -0
- package/dist/{chunk-EMQSAY3B.js → chunk-O6ASDHFO.js} +2 -1
- package/dist/{chunk-2TJGRJ4O.js → chunk-PUVXOZ6T.js} +8 -2
- package/dist/{chunk-4KPUF5JD.js → chunk-TIWH32HP.js} +15 -2
- package/dist/chunk-UU7A7KLB.js +58 -0
- package/dist/cli.js +19 -9
- package/dist/{daemon-restart-JMZM3QY4.js → daemon-restart-KPSWNYTH.js} +3 -3
- package/dist/daemon.js +1802 -1082
- package/dist/{db-5ZVC6MQF.js → db-C2CJ46ZU.js} +2 -2
- package/dist/{delivery-manager-ISTJMZDW.js → delivery-manager-CSG7LXA4.js} +3 -3
- package/dist/{export-GCDNQCF3.js → export-6QBUOQGC.js} +2 -2
- package/dist/file-C57SK5DK.js +204 -0
- package/dist/{import-M63VIUJ5.js → import-XEC34Y4Z.js} +1 -1
- package/dist/{mind-PQ5NCPSU.js → mind-Z7CKD6DG.js} +2 -2
- package/dist/mind-activity-tracker-624QLQLC.js +19 -0
- package/dist/{mind-manager-RVCFROAY.js → mind-manager-3DMYKZPB.js} +3 -3
- package/dist/{package-MYE2ZJLV.js → package-4NHAVUUI.js} +1 -1
- package/dist/{pages-AXCOSY3P.js → pages-4DGQT7ZA.js} +2 -2
- package/dist/{publish-YB377JB7.js → publish-TAJUET4I.js} +7 -4
- package/dist/{schedule-LMX7GAQZ.js → schedule-FFZG23IW.js} +25 -5
- package/dist/{schema-5BW7DFZI.js → schema-GFH6RV3W.js} +3 -1
- package/dist/{setup-OH3PJUJO.js → setup-52YRV7VP.js} +16 -0
- package/dist/skills/volute-mind/SKILL.md +33 -3
- package/dist/{sprout-VBEX63LX.js → sprout-QN7Y4VVO.js} +3 -3
- package/dist/{status-JCJAOXTW.js → status-FU2PFVVF.js} +3 -2
- package/dist/{up-WG65SWJU.js → up-FS7CKM6V.js} +1 -1
- package/dist/web-assets/assets/index-CUZTZzaW.js +64 -0
- package/dist/web-assets/assets/index-adVuCkqy.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0012_activity.sql +11 -0
- package/drizzle/meta/0012_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/_base/home/.config/routes.json +2 -2
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/daemon-client.ts +22 -0
- package/templates/_base/src/lib/transparency.ts +1 -1
- package/templates/claude/.init/.config/routes.json +7 -1
- package/templates/pi/.init/.config/routes.json +7 -1
- package/templates/pi/src/agent.ts +11 -5
- package/templates/pi/src/lib/session-context-extension.ts +6 -4
- package/templates/pi/src/server.ts +2 -0
- package/dist/web-assets/assets/index-BAbuRsVF.css +0 -1
- package/dist/web-assets/assets/index-CiQhSKi_.js +0 -63
- /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
|
|
3
|
+
A platform for AI minds — persistent, self-modifying, and free to communicate with each other and the world.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
|
16
|
+
# Start the daemon (manages all your minds)
|
|
17
17
|
volute up
|
|
18
18
|
|
|
19
|
-
# Create
|
|
20
|
-
volute
|
|
19
|
+
# Create a mind
|
|
20
|
+
volute mind create atlas
|
|
21
21
|
|
|
22
22
|
# Start it
|
|
23
|
-
volute
|
|
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
|
|
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
|
|
39
|
-
volute status # check daemon status, version, and
|
|
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
|
|
42
|
+
The daemon handles mind lifecycle, crash recovery (auto-restarts after 3 seconds), connector processes, scheduled messages, and the web dashboard.
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Minds
|
|
45
45
|
|
|
46
46
|
### Lifecycle
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
volute
|
|
50
|
-
volute
|
|
51
|
-
volute
|
|
52
|
-
volute
|
|
53
|
-
volute
|
|
54
|
-
volute
|
|
55
|
-
volute
|
|
56
|
-
volute
|
|
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
|
|
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
|
|
67
|
+
### Anatomy of a mind
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
~/.volute/
|
|
71
|
-
├── home/ # the
|
|
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/ #
|
|
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
|
|
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
|
|
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
|
|
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
|
|
86
|
+
**Auto-commit**: any file changes the mind makes inside `home/` are automatically committed to git.
|
|
87
87
|
|
|
88
|
-
**Session resume**: if the
|
|
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.
|
|
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 --
|
|
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 --
|
|
102
|
+
volute variant list --mind atlas
|
|
103
103
|
|
|
104
|
-
# Merge it back (verifies, merges, cleans up, restarts the main
|
|
105
|
-
volute variant merge experiment --
|
|
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
|
|
113
|
-
4. After restart, the
|
|
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 --
|
|
118
|
+
volute variant create poet --mind atlas --soul "You are a poet who responds only in verse."
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
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
|
|
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
|
|
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 --
|
|
134
|
+
volute connector connect discord --mind atlas
|
|
135
135
|
|
|
136
136
|
# Disconnect
|
|
137
|
-
volute connector disconnect discord --
|
|
137
|
+
volute connector disconnect discord --mind atlas
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
The
|
|
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 --
|
|
148
|
-
volute send discord:123456789 "hello" --
|
|
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 --
|
|
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 --
|
|
161
|
-
volute schedule remove --
|
|
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
|
|
172
|
+
volute register --name my-system
|
|
173
173
|
|
|
174
174
|
# Or log in with an existing key
|
|
175
|
-
volute
|
|
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
|
|
191
|
+
volute logout # remove stored credentials
|
|
192
192
|
```
|
|
193
193
|
|
|
194
194
|
## Environment variables
|
|
195
195
|
|
|
196
|
-
Manage secrets and config. Supports shared (all
|
|
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 --
|
|
201
|
-
volute env list --
|
|
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
|
|
216
|
+
## Upgrading minds
|
|
217
217
|
|
|
218
|
-
When the Volute template updates, you can upgrade
|
|
218
|
+
When the Volute template updates, you can upgrade minds without touching their identity:
|
|
219
219
|
|
|
220
220
|
```sh
|
|
221
|
-
volute
|
|
221
|
+
volute mind upgrade atlas # creates an "upgrade" variant
|
|
222
222
|
# resolve conflicts if needed, then:
|
|
223
|
-
volute
|
|
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 --
|
|
227
|
+
volute variant merge upgrade --mind atlas
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
Your
|
|
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
|
-
- **`
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
+
};
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
} from "./chunk-YUIHSKR6.js";
|
|
5
5
|
import {
|
|
6
6
|
getDb
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5XNT2472.js";
|
|
8
8
|
import {
|
|
9
9
|
deliveryQueue,
|
|
10
10
|
mindHistory
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
689
|
-
|
|
690
|
-
|
|
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) ?? {
|
|
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
|