volute 0.3.1 → 0.5.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 (82) hide show
  1. package/README.md +29 -29
  2. package/dist/agent-Z2B6EFEQ.js +75 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
  4. package/dist/channel-MK5OK2SI.js +113 -0
  5. package/dist/chunk-5X7HGB6L.js +107 -0
  6. package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
  7. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  8. package/dist/chunk-B3R6L2GW.js +24 -0
  9. package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
  10. package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
  11. package/dist/chunk-MXUCNIBG.js +168 -0
  12. package/dist/chunk-SMISE4SV.js +226 -0
  13. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  14. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  15. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  16. package/dist/chunk-ZYGKG6VC.js +22 -0
  17. package/dist/cli.js +98 -75
  18. package/dist/connector-LYEMXQEV.js +157 -0
  19. package/dist/connectors/discord.js +104 -161
  20. package/dist/connectors/slack.js +179 -0
  21. package/dist/connectors/telegram.js +175 -0
  22. package/dist/conversation-ERXEQZTY.js +163 -0
  23. package/dist/create-RVCZN6HE.js +91 -0
  24. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  25. package/dist/daemon.js +824 -252
  26. package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
  27. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  28. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  29. package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
  30. package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
  31. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  32. package/dist/message-ADHWFHSI.js +32 -0
  33. package/dist/package-VQOE7JNH.js +89 -0
  34. package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
  35. package/dist/send-66QMKRUH.js +75 -0
  36. package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
  37. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  40. package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
  41. package/dist/update-LPSIAWQ2.js +140 -0
  42. package/dist/update-check-Y33QDCFL.js +17 -0
  43. package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
  44. package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
  45. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  46. package/dist/web-assets/index.html +2 -2
  47. package/drizzle/0003_clean_ego.sql +12 -0
  48. package/drizzle/meta/0003_snapshot.json +417 -0
  49. package/drizzle/meta/_journal.json +7 -0
  50. package/package.json +3 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
  53. package/templates/_base/home/.config/routes.json +10 -0
  54. package/templates/_base/home/VOLUTE.md +19 -28
  55. package/templates/_base/src/lib/file-handler.ts +46 -0
  56. package/templates/_base/src/lib/format-prefix.ts +1 -1
  57. package/templates/_base/src/lib/router.ts +327 -0
  58. package/templates/_base/src/lib/routing.ts +137 -0
  59. package/templates/_base/src/lib/types.ts +16 -3
  60. package/templates/_base/src/lib/volute-server.ts +20 -48
  61. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  62. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  63. package/templates/agent-sdk/src/agent.ts +269 -82
  64. package/templates/agent-sdk/src/server.ts +19 -4
  65. package/templates/agent-sdk/volute-template.json +1 -1
  66. package/templates/pi/.init/.config/routes.json +5 -0
  67. package/templates/pi/.init/AGENTS.md +1 -1
  68. package/templates/pi/src/agent.ts +279 -58
  69. package/templates/pi/src/server.ts +15 -4
  70. package/templates/pi/volute-template.json +1 -1
  71. package/dist/channel-7FZ6D25H.js +0 -90
  72. package/dist/chunk-N4YNKR3Q.js +0 -90
  73. package/dist/connector-TVJULIRT.js +0 -96
  74. package/dist/create-BRG2DBWI.js +0 -79
  75. package/dist/send-UK3JBZIB.js +0 -53
  76. package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
  77. package/templates/_base/src/lib/sessions.ts +0 -71
  78. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  79. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  80. package/templates/pi/.init/.config/sessions.json +0 -1
  81. package/templates/pi/src/lib/agent-sessions.ts +0 -210
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
@@ -1,15 +1,58 @@
1
- import { createSessionManager } from "./lib/agent-sessions.js";
2
- import { formatPrefix } from "./lib/format-prefix.js";
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
3
+ import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
4
+ import { query } from "@anthropic-ai/claude-agent-sdk";
3
5
  import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
4
6
  import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
5
- import { logMessage } from "./lib/logger.js";
6
- import {
7
- type ChannelMeta,
8
- INTERACTIVE_CHANNELS,
9
- type Listener,
10
- type VoluteContentPart,
7
+ import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
8
+ import { log, logText, logThinking, logToolUse } from "./lib/logger.js";
9
+ import { createMessageChannel } from "./lib/message-channel.js";
10
+ import type {
11
+ HandlerMeta,
12
+ HandlerResolver,
13
+ Listener,
14
+ MessageHandler,
15
+ VoluteContentPart,
16
+ VoluteEvent,
11
17
  } from "./lib/types.js";
12
18
 
19
+ type SDKContent = (
20
+ | { type: "text"; text: string }
21
+ | {
22
+ type: "image";
23
+ source: {
24
+ type: "base64";
25
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
26
+ data: string;
27
+ };
28
+ }
29
+ )[];
30
+
31
+ type Session = {
32
+ name: string;
33
+ channel: ReturnType<typeof createMessageChannel>;
34
+ listeners: Set<Listener>;
35
+ messageIds: (string | undefined)[];
36
+ currentMessageId?: string;
37
+ currentQuery?: ReturnType<typeof query>;
38
+ };
39
+
40
+ function toSDKContent(content: VoluteContentPart[]): SDKContent {
41
+ return content.map((part) => {
42
+ if (part.type === "text") {
43
+ return { type: "text" as const, text: part.text };
44
+ }
45
+ return {
46
+ type: "image" as const,
47
+ source: {
48
+ type: "base64" as const,
49
+ media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
50
+ data: part.data,
51
+ },
52
+ };
53
+ });
54
+ }
55
+
13
56
  export function createAgent(options: {
14
57
  systemPrompt: string;
15
58
  cwd: string;
@@ -18,95 +61,239 @@ export function createAgent(options: {
18
61
  sessionsDir: string;
19
62
  compactionMessage?: string;
20
63
  onIdentityReload?: () => Promise<void>;
21
- }) {
64
+ }): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
22
65
  const autoCommit = createAutoCommitHook(options.cwd);
23
66
  const identityReload = createIdentityReloadHook(options.cwd);
67
+ const postToolUseHooks: { matcher: string; hooks: HookCallback[] }[] = [
68
+ { matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] },
69
+ ];
24
70
 
25
- const sessionManager = createSessionManager({
26
- systemPrompt: options.systemPrompt,
27
- cwd: options.cwd,
28
- abortController: options.abortController,
29
- model: options.model,
30
- sessionsDir: options.sessionsDir,
31
- compactionMessage: options.compactionMessage,
32
- postToolUseHooks: [{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] }],
33
- onTurnDone: () => {
34
- if (identityReload.needsReload()) {
35
- options.onIdentityReload?.();
71
+ const sessions = new Map<string, Session>();
72
+ const today = new Date().toISOString().slice(0, 10);
73
+ const compactionMessage =
74
+ options.compactionMessage ??
75
+ `Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
76
+
77
+ // --- Session persistence ---
78
+
79
+ function sessionFilePath(sessionName: string): string {
80
+ return resolvePath(options.sessionsDir, `${sessionName}.json`);
81
+ }
82
+
83
+ function loadSessionId(sessionName: string): string | undefined {
84
+ try {
85
+ const data = JSON.parse(readFileSync(sessionFilePath(sessionName), "utf-8"));
86
+ return data.sessionId;
87
+ } catch (err: any) {
88
+ if (err?.code !== "ENOENT") {
89
+ log("agent", `failed to load session file for "${sessionName}":`, err);
36
90
  }
37
- },
38
- });
91
+ return undefined;
92
+ }
93
+ }
39
94
 
40
- function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
41
- const sessionName = meta?.sessionName ?? "main";
42
- const session = sessionManager.getOrCreateSession(sessionName);
43
-
44
- const text =
45
- typeof content === "string"
46
- ? content
47
- : content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
48
- logMessage("in", text, meta?.channel);
49
-
50
- const time = new Date().toLocaleString();
51
- const prefix = formatPrefix(meta, time);
52
-
53
- let sdkContent: (
54
- | { type: "text"; text: string }
55
- | {
56
- type: "image";
57
- source: {
58
- type: "base64";
59
- media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
60
- data: string;
61
- };
62
- }
63
- )[];
95
+ function saveSessionId(sessionName: string, sessionId: string) {
96
+ mkdirSync(options.sessionsDir, { recursive: true });
97
+ writeFileSync(sessionFilePath(sessionName), JSON.stringify({ sessionId }));
98
+ }
64
99
 
65
- if (typeof content === "string") {
66
- sdkContent = [{ type: "text" as const, text: prefix + content }];
67
- } else {
68
- const hasText = content.some((p) => p.type === "text");
69
- sdkContent = content.map((part, i) => {
70
- if (part.type === "text") {
71
- return { type: "text" as const, text: (i === 0 ? prefix : "") + part.text };
72
- }
73
- return {
74
- type: "image" as const,
75
- source: {
76
- type: "base64" as const,
77
- media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
78
- data: part.data,
79
- },
80
- };
100
+ function deleteSessionId(sessionName: string) {
101
+ try {
102
+ const path = sessionFilePath(sessionName);
103
+ if (existsSync(path)) unlinkSync(path);
104
+ } catch (err) {
105
+ log("agent", `failed to delete session file for "${sessionName}":`, err);
106
+ }
107
+ }
108
+
109
+ // --- Event broadcasting ---
110
+
111
+ function broadcastToSession(session: Session, event: VoluteEvent) {
112
+ const tagged =
113
+ session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
114
+ for (const listener of session.listeners) {
115
+ try {
116
+ listener(tagged);
117
+ } catch (err) {
118
+ log("agent", "listener threw during broadcast:", err);
119
+ }
120
+ }
121
+ }
122
+
123
+ // --- SDK stream management ---
124
+
125
+ function createStream(session: Session, resume?: string) {
126
+ const preCompact = createPreCompactHook(() => {
127
+ session.messageIds.push(undefined);
128
+ session.channel.push({
129
+ type: "user",
130
+ session_id: "",
131
+ message: {
132
+ role: "user",
133
+ content: [{ type: "text", text: compactionMessage }],
134
+ },
135
+ parent_tool_use_id: null,
81
136
  });
82
- if (prefix && !hasText) {
83
- sdkContent.unshift({ type: "text" as const, text: prefix.trimEnd() });
137
+ });
138
+
139
+ return query({
140
+ prompt: session.channel.iterable,
141
+ options: {
142
+ systemPrompt: options.systemPrompt,
143
+ permissionMode: "bypassPermissions",
144
+ allowDangerouslySkipPermissions: true,
145
+ settingSources: ["project"],
146
+ cwd: options.cwd,
147
+ abortController: options.abortController,
148
+ model: options.model,
149
+ resume,
150
+ hooks: {
151
+ PostToolUse: postToolUseHooks,
152
+ PreCompact: [{ hooks: [preCompact.hook] }],
153
+ },
154
+ },
155
+ });
156
+ }
157
+
158
+ async function consumeStream(stream: ReturnType<typeof query>, session: Session) {
159
+ for await (const msg of stream) {
160
+ if (session.currentMessageId === undefined) {
161
+ session.currentMessageId = session.messageIds.shift();
162
+ }
163
+ if ("session_id" in msg && msg.session_id) {
164
+ if (!session.name.startsWith("new-")) {
165
+ saveSessionId(session.name, msg.session_id as string);
166
+ }
167
+ }
168
+ if (msg.type === "assistant") {
169
+ for (const b of msg.message.content) {
170
+ if (b.type === "thinking" && "thinking" in b && b.thinking) {
171
+ logThinking(b.thinking as string);
172
+ } else if (b.type === "text") {
173
+ const text = (b as { text: string }).text;
174
+ logText(text);
175
+ broadcastToSession(session, { type: "text", content: text });
176
+ } else if (b.type === "tool_use") {
177
+ const tb = b as { name: string; input: unknown };
178
+ logToolUse(tb.name, tb.input);
179
+ broadcastToSession(session, { type: "tool_use", name: tb.name, input: tb.input });
180
+ }
181
+ }
182
+ }
183
+ if (msg.type === "result") {
184
+ log("agent", `session "${session.name}": turn done`);
185
+ broadcastToSession(session, { type: "done" });
186
+ session.currentMessageId = undefined;
187
+ if (identityReload.needsReload()) {
188
+ options.onIdentityReload?.();
189
+ }
84
190
  }
85
191
  }
192
+ }
193
+
194
+ function startSession(session: Session, savedSessionId?: string) {
195
+ (async () => {
196
+ log("agent", `session "${session.name}": stream consumer started`);
197
+ try {
198
+ const q = createStream(session, savedSessionId);
199
+ session.currentQuery = q;
200
+ await consumeStream(q, session);
201
+ } catch (err) {
202
+ if (savedSessionId) {
203
+ log("agent", `session "${session.name}": resume failed, starting fresh:`, err);
204
+ deleteSessionId(session.name);
205
+ try {
206
+ const q = createStream(session);
207
+ session.currentQuery = q;
208
+ await consumeStream(q, session);
209
+ } catch (retryErr) {
210
+ log("agent", `session "${session.name}": stream consumer error:`, retryErr);
211
+ broadcastToSession(session, { type: "done" });
212
+ sessions.delete(session.name);
213
+ }
214
+ } else {
215
+ log("agent", `session "${session.name}": stream consumer error:`, err);
216
+ broadcastToSession(session, { type: "done" });
217
+ sessions.delete(session.name);
218
+ }
219
+ }
220
+ log("agent", `session "${session.name}": stream consumer ended`);
221
+ })();
222
+ }
223
+
224
+ function getOrCreateSession(name: string): Session {
225
+ const existing = sessions.get(name);
226
+ if (existing) return existing;
227
+
228
+ const session: Session = {
229
+ name,
230
+ channel: createMessageChannel(),
231
+ listeners: new Set(),
232
+ messageIds: [],
233
+ };
234
+ sessions.set(name, session);
86
235
 
87
- // Interrupt current turn for interactive channels so the new message is processed immediately
88
- if (INTERACTIVE_CHANNELS.has(meta?.channel ?? "") && session.currentMessageId !== undefined) {
89
- sessionManager.interruptSession(sessionName);
236
+ const isEphemeral = name.startsWith("new-");
237
+ const savedSessionId = isEphemeral ? undefined : loadSessionId(name);
238
+ if (savedSessionId) {
239
+ log("agent", `session "${name}": resuming ${savedSessionId}`);
240
+ } else {
241
+ log("agent", `session "${name}": starting fresh`);
90
242
  }
91
243
 
92
- session.messageIds.push(meta?.messageId);
93
- session.channel.push({
94
- type: "user",
95
- session_id: "",
96
- message: {
97
- role: "user",
98
- content: sdkContent,
244
+ startSession(session, savedSessionId);
245
+ return session;
246
+ }
247
+
248
+ // --- MessageHandler implementation ---
249
+
250
+ function createSessionHandler(sessionName: string): MessageHandler {
251
+ return {
252
+ handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void {
253
+ const session = getOrCreateSession(sessionName);
254
+
255
+ // Filter listener to only receive events for this messageId
256
+ const filteredListener: Listener = (event) => {
257
+ if (event.messageId === meta.messageId) listener(event);
258
+ };
259
+ session.listeners.add(filteredListener);
260
+
261
+ // Interrupt if requested and session is mid-turn
262
+ if (meta.interrupt && session.currentMessageId !== undefined && session.currentQuery) {
263
+ log("agent", `session "${sessionName}": interrupting current turn`);
264
+ session.currentQuery.interrupt();
265
+ }
266
+
267
+ // Push message into SDK
268
+ session.messageIds.push(meta.messageId);
269
+ session.channel.push({
270
+ type: "user",
271
+ session_id: "",
272
+ message: { role: "user", content: toSDKContent(content) },
273
+ parent_tool_use_id: null,
274
+ });
275
+
276
+ return () => session.listeners.delete(filteredListener);
99
277
  },
100
- parent_tool_use_id: null,
101
- });
278
+ };
102
279
  }
103
280
 
104
- function onMessage(listener: Listener, sessionName?: string): () => void {
105
- const name = sessionName ?? "main";
106
- const session = sessionManager.getOrCreateSession(name);
107
- session.listeners.add(listener);
108
- return () => session.listeners.delete(listener);
281
+ // --- HandlerResolver ---
282
+
283
+ const handlers = new Map<string, MessageHandler>();
284
+
285
+ function resolve(sessionName: string): MessageHandler {
286
+ // Ephemeral sessions get unique names — don't cache their handlers
287
+ if (sessionName.startsWith("new-")) {
288
+ return createSessionHandler(sessionName);
289
+ }
290
+ let handler = handlers.get(sessionName);
291
+ if (!handler) {
292
+ handler = createSessionHandler(sessionName);
293
+ handlers.set(sessionName, handler);
294
+ }
295
+ return handler;
109
296
  }
110
297
 
111
- return { sendMessage, onMessage, waitForCommits: autoCommit.waitForCommits };
298
+ return { resolve, waitForCommits: autoCommit.waitForCommits };
112
299
  }
@@ -1,7 +1,9 @@
1
- import { existsSync, mkdirSync, renameSync } from "node:fs";
1
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { createAgent } from "./agent.js";
4
+ import { createFileHandlerResolver } from "./lib/file-handler.js";
4
5
  import { log } from "./lib/logger.js";
6
+ import { createRouter } from "./lib/router.js";
5
7
  import {
6
8
  handleMergeContext,
7
9
  loadConfig,
@@ -39,24 +41,37 @@ const agent = createAgent({
39
41
  onIdentityReload: async () => {
40
42
  log("server", "identity file changed — restarting to reload");
41
43
  await agent.waitForCommits();
44
+ // Signal daemon to restart immediately (bypasses crash backoff)
45
+ try {
46
+ writeFileSync(resolve(".volute/restart.json"), JSON.stringify({ action: "reload" }));
47
+ } catch (err) {
48
+ log("server", "failed to write restart signal:", err);
49
+ }
42
50
  server.close();
43
51
  process.exit(0);
44
52
  },
45
53
  });
46
54
 
55
+ const router = createRouter({
56
+ configPath: resolve("home/.config/routes.json"),
57
+ agentHandler: agent.resolve,
58
+ fileHandler: createFileHandlerResolver(resolve("home")),
59
+ });
60
+
47
61
  const server = createVoluteServer({
48
- agent,
62
+ router,
49
63
  port,
50
64
  name: pkg.name,
51
65
  version: pkg.version,
52
- sessionsConfigPath: resolve("home/.config/sessions.json"),
53
66
  });
54
67
 
55
68
  server.listen(port, () => {
56
69
  const addr = server.address();
57
70
  const actualPort = typeof addr === "object" && addr ? addr.port : port;
58
71
  log("server", `listening on :${actualPort}`);
59
- handleMergeContext((content) => agent.sendMessage(content));
72
+ handleMergeContext((content) =>
73
+ router.route([{ type: "text", text: content }], { channel: "system" }),
74
+ );
60
75
  });
61
76
 
62
77
  setupShutdown();
@@ -4,6 +4,6 @@
4
4
  "biome.json.tmpl": "biome.json",
5
5
  "home/.config/volute.json.tmpl": "home/.config/volute.json"
6
6
  },
7
- "substitute": ["package.json", ".init/SOUL.md"],
7
+ "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"],
8
8
  "skillsDir": "home/.claude/skills"
9
9
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "gateUnmatched": true,
3
+ "rules": [{ "channel": "volute:*", "isDM": true, "session": "${channel}" }],
4
+ "default": "main"
5
+ }
@@ -23,7 +23,7 @@ See the **memory** skill for detailed guidance.
23
23
 
24
24
  ## Sessions
25
25
 
26
- - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/sessions.json`.
26
+ - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
27
27
  - Your conversation may be **resumed** from a previous session — orient yourself by reading recent daily logs if needed.
28
28
  - On a **fresh session**, read `MEMORY.md` and recent daily logs to remember where you left off.
29
29
  - On **compaction**, update today's daily log to preserve context before the conversation is trimmed.