@poolzin/pool-bot 2026.2.23 → 2026.2.25

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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,34 +1,220 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
- import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
3
+ import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
5
4
  import { resolveSessionAgentId } from "../../agents/agent-scope.js";
6
- import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js";
7
5
  import { resolveThinkingDefault } from "../../agents/model-selection.js";
8
6
  import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
9
7
  import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
10
8
  import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
11
- import { extractShortModelName, } from "../../auto-reply/reply/response-prefix-template.js";
9
+ import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
10
+ import { resolveSessionFilePath } from "../../config/sessions.js";
12
11
  import { resolveSendPolicy } from "../../sessions/send-policy.js";
13
12
  import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
14
13
  import { abortChatRunById, abortChatRunsForSessionKey, isChatStopCommandText, resolveChatRunExpiresAtMs, } from "../chat-abort.js";
15
14
  import { parseMessageWithAttachments } from "../chat-attachments.js";
15
+ import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
16
+ import { GATEWAY_CLIENT_CAPS, hasGatewayClientCap } from "../protocol/client-info.js";
16
17
  import { ErrorCodes, errorShape, formatValidationErrors, validateChatAbortParams, validateChatHistoryParams, validateChatInjectParams, validateChatSendParams, } from "../protocol/index.js";
17
18
  import { getMaxChatHistoryMessagesBytes } from "../server-constants.js";
18
19
  import { capArrayByJsonBytes, loadSessionEntry, readSessionMessages, resolveSessionModelRef, } from "../session-utils.js";
19
- import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
20
20
  import { formatForLog } from "../ws-log.js";
21
+ import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js";
22
+ import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js";
23
+ const CHAT_HISTORY_TEXT_MAX_CHARS = 12_000;
24
+ const CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES = 128 * 1024;
25
+ const CHAT_HISTORY_OVERSIZED_PLACEHOLDER = "[chat.history omitted: message too large]";
26
+ let chatHistoryPlaceholderEmitCount = 0;
27
+ function stripDisallowedChatControlChars(message) {
28
+ let output = "";
29
+ for (const char of message) {
30
+ const code = char.charCodeAt(0);
31
+ if (code === 9 || code === 10 || code === 13 || (code >= 32 && code !== 127)) {
32
+ output += char;
33
+ }
34
+ }
35
+ return output;
36
+ }
37
+ export function sanitizeChatSendMessageInput(message) {
38
+ const normalized = message.normalize("NFC");
39
+ if (normalized.includes("\u0000")) {
40
+ return { ok: false, error: "message must not contain null bytes" };
41
+ }
42
+ return { ok: true, message: stripDisallowedChatControlChars(normalized) };
43
+ }
44
+ function truncateChatHistoryText(text) {
45
+ if (text.length <= CHAT_HISTORY_TEXT_MAX_CHARS) {
46
+ return { text, truncated: false };
47
+ }
48
+ return {
49
+ text: `${text.slice(0, CHAT_HISTORY_TEXT_MAX_CHARS)}\n...(truncated)...`,
50
+ truncated: true,
51
+ };
52
+ }
53
+ function sanitizeChatHistoryContentBlock(block) {
54
+ if (!block || typeof block !== "object") {
55
+ return { block, changed: false };
56
+ }
57
+ const entry = { ...block };
58
+ let changed = false;
59
+ if (typeof entry.text === "string") {
60
+ const res = truncateChatHistoryText(entry.text);
61
+ entry.text = res.text;
62
+ changed ||= res.truncated;
63
+ }
64
+ if (typeof entry.partialJson === "string") {
65
+ const res = truncateChatHistoryText(entry.partialJson);
66
+ entry.partialJson = res.text;
67
+ changed ||= res.truncated;
68
+ }
69
+ if (typeof entry.arguments === "string") {
70
+ const res = truncateChatHistoryText(entry.arguments);
71
+ entry.arguments = res.text;
72
+ changed ||= res.truncated;
73
+ }
74
+ if (typeof entry.thinking === "string") {
75
+ const res = truncateChatHistoryText(entry.thinking);
76
+ entry.thinking = res.text;
77
+ changed ||= res.truncated;
78
+ }
79
+ if ("thinkingSignature" in entry) {
80
+ delete entry.thinkingSignature;
81
+ changed = true;
82
+ }
83
+ const type = typeof entry.type === "string" ? entry.type : "";
84
+ if (type === "image" && typeof entry.data === "string") {
85
+ const bytes = Buffer.byteLength(entry.data, "utf8");
86
+ delete entry.data;
87
+ entry.omitted = true;
88
+ entry.bytes = bytes;
89
+ changed = true;
90
+ }
91
+ return { block: changed ? entry : block, changed };
92
+ }
93
+ function sanitizeChatHistoryMessage(message) {
94
+ if (!message || typeof message !== "object") {
95
+ return { message, changed: false };
96
+ }
97
+ const entry = { ...message };
98
+ let changed = false;
99
+ if ("details" in entry) {
100
+ delete entry.details;
101
+ changed = true;
102
+ }
103
+ if ("usage" in entry) {
104
+ delete entry.usage;
105
+ changed = true;
106
+ }
107
+ if ("cost" in entry) {
108
+ delete entry.cost;
109
+ changed = true;
110
+ }
111
+ if (typeof entry.content === "string") {
112
+ const res = truncateChatHistoryText(entry.content);
113
+ entry.content = res.text;
114
+ changed ||= res.truncated;
115
+ }
116
+ else if (Array.isArray(entry.content)) {
117
+ const updated = entry.content.map((block) => sanitizeChatHistoryContentBlock(block));
118
+ if (updated.some((item) => item.changed)) {
119
+ entry.content = updated.map((item) => item.block);
120
+ changed = true;
121
+ }
122
+ }
123
+ if (typeof entry.text === "string") {
124
+ const res = truncateChatHistoryText(entry.text);
125
+ entry.text = res.text;
126
+ changed ||= res.truncated;
127
+ }
128
+ return { message: changed ? entry : message, changed };
129
+ }
130
+ function sanitizeChatHistoryMessages(messages) {
131
+ if (messages.length === 0) {
132
+ return messages;
133
+ }
134
+ let changed = false;
135
+ const next = messages.map((message) => {
136
+ const res = sanitizeChatHistoryMessage(message);
137
+ changed ||= res.changed;
138
+ return res.message;
139
+ });
140
+ return changed ? next : messages;
141
+ }
142
+ function jsonUtf8Bytes(value) {
143
+ try {
144
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
145
+ }
146
+ catch {
147
+ return Buffer.byteLength(String(value), "utf8");
148
+ }
149
+ }
150
+ function buildOversizedHistoryPlaceholder(message) {
151
+ const role = message &&
152
+ typeof message === "object" &&
153
+ typeof message.role === "string"
154
+ ? message.role
155
+ : "assistant";
156
+ const timestamp = message &&
157
+ typeof message === "object" &&
158
+ typeof message.timestamp === "number"
159
+ ? message.timestamp
160
+ : Date.now();
161
+ return {
162
+ role,
163
+ timestamp,
164
+ content: [{ type: "text", text: CHAT_HISTORY_OVERSIZED_PLACEHOLDER }],
165
+ __poolbot: { truncated: true, reason: "oversized" },
166
+ };
167
+ }
168
+ function replaceOversizedChatHistoryMessages(params) {
169
+ const { messages, maxSingleMessageBytes } = params;
170
+ if (messages.length === 0) {
171
+ return { messages, replacedCount: 0 };
172
+ }
173
+ let replacedCount = 0;
174
+ const next = messages.map((message) => {
175
+ if (jsonUtf8Bytes(message) <= maxSingleMessageBytes) {
176
+ return message;
177
+ }
178
+ replacedCount += 1;
179
+ return buildOversizedHistoryPlaceholder(message);
180
+ });
181
+ return { messages: replacedCount > 0 ? next : messages, replacedCount };
182
+ }
183
+ function enforceChatHistoryFinalBudget(params) {
184
+ const { messages, maxBytes } = params;
185
+ if (messages.length === 0) {
186
+ return { messages, placeholderCount: 0 };
187
+ }
188
+ if (jsonUtf8Bytes(messages) <= maxBytes) {
189
+ return { messages, placeholderCount: 0 };
190
+ }
191
+ const last = messages.at(-1);
192
+ if (last && jsonUtf8Bytes([last]) <= maxBytes) {
193
+ return { messages: [last], placeholderCount: 0 };
194
+ }
195
+ const placeholder = buildOversizedHistoryPlaceholder(last);
196
+ if (jsonUtf8Bytes([placeholder]) <= maxBytes) {
197
+ return { messages: [placeholder], placeholderCount: 1 };
198
+ }
199
+ return { messages: [], placeholderCount: 0 };
200
+ }
21
201
  function resolveTranscriptPath(params) {
22
- const { sessionId, storePath, sessionFile } = params;
23
- if (sessionFile)
24
- return sessionFile;
25
- if (!storePath)
202
+ const { sessionId, storePath, sessionFile, agentId } = params;
203
+ if (!storePath && !sessionFile) {
26
204
  return null;
27
- return path.join(path.dirname(storePath), `${sessionId}.jsonl`);
205
+ }
206
+ try {
207
+ const sessionsDir = storePath ? path.dirname(storePath) : undefined;
208
+ return resolveSessionFilePath(sessionId, sessionFile ? { sessionFile } : undefined, sessionsDir || agentId ? { sessionsDir, agentId } : undefined);
209
+ }
210
+ catch {
211
+ return null;
212
+ }
28
213
  }
29
214
  function ensureTranscriptFile(params) {
30
- if (fs.existsSync(params.transcriptPath))
215
+ if (fs.existsSync(params.transcriptPath)) {
31
216
  return { ok: true };
217
+ }
32
218
  try {
33
219
  fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true });
34
220
  const header = {
@@ -38,18 +224,40 @@ function ensureTranscriptFile(params) {
38
224
  timestamp: new Date().toISOString(),
39
225
  cwd: process.cwd(),
40
226
  };
41
- fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, "utf-8");
227
+ fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, {
228
+ encoding: "utf-8",
229
+ mode: 0o600,
230
+ });
42
231
  return { ok: true };
43
232
  }
44
233
  catch (err) {
45
234
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
46
235
  }
47
236
  }
237
+ function transcriptHasIdempotencyKey(transcriptPath, idempotencyKey) {
238
+ try {
239
+ const lines = fs.readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
240
+ for (const line of lines) {
241
+ if (!line.trim()) {
242
+ continue;
243
+ }
244
+ const parsed = JSON.parse(line);
245
+ if (parsed?.message?.idempotencyKey === idempotencyKey) {
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ }
48
255
  function appendAssistantTranscriptMessage(params) {
49
256
  const transcriptPath = resolveTranscriptPath({
50
257
  sessionId: params.sessionId,
51
258
  storePath: params.storePath,
52
259
  sessionFile: params.sessionFile,
260
+ agentId: params.agentId,
53
261
  });
54
262
  if (!transcriptPath) {
55
263
  return { ok: false, error: "transcript path not resolved" };
@@ -66,29 +274,134 @@ function appendAssistantTranscriptMessage(params) {
66
274
  return { ok: false, error: ensured.error ?? "failed to create transcript file" };
67
275
  }
68
276
  }
277
+ if (params.idempotencyKey && transcriptHasIdempotencyKey(transcriptPath, params.idempotencyKey)) {
278
+ return { ok: true };
279
+ }
69
280
  const now = Date.now();
70
- const messageId = randomUUID().slice(0, 8);
71
281
  const labelPrefix = params.label ? `[${params.label}]\n\n` : "";
282
+ const usage = {
283
+ input: 0,
284
+ output: 0,
285
+ cacheRead: 0,
286
+ cacheWrite: 0,
287
+ totalTokens: 0,
288
+ cost: {
289
+ input: 0,
290
+ output: 0,
291
+ cacheRead: 0,
292
+ cacheWrite: 0,
293
+ total: 0,
294
+ },
295
+ };
72
296
  const messageBody = {
73
297
  role: "assistant",
74
298
  content: [{ type: "text", text: `${labelPrefix}${params.message}` }],
75
299
  timestamp: now,
76
- stopReason: "injected",
77
- usage: { input: 0, output: 0, totalTokens: 0 },
78
- };
79
- const transcriptEntry = {
80
- type: "message",
81
- id: messageId,
82
- timestamp: new Date(now).toISOString(),
83
- message: messageBody,
300
+ // Pi stopReason is a strict enum; this is not model output, but we still store it as a
301
+ // normal assistant message so it participates in the session parentId chain.
302
+ stopReason: "stop",
303
+ usage,
304
+ // Make these explicit so downstream tooling never treats this as model output.
305
+ api: "openai-responses",
306
+ provider: "poolbot",
307
+ model: "gateway-injected",
308
+ ...(params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}),
309
+ ...(params.abortMeta
310
+ ? {
311
+ poolbotAbort: {
312
+ aborted: true,
313
+ origin: params.abortMeta.origin,
314
+ runId: params.abortMeta.runId,
315
+ },
316
+ }
317
+ : {}),
84
318
  };
85
319
  try {
86
- fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8");
320
+ // IMPORTANT: Use SessionManager so the entry is attached to the current leaf via parentId.
321
+ // Raw jsonl appends break the parent chain and can hide compaction summaries from context.
322
+ const sessionManager = SessionManager.open(transcriptPath);
323
+ const messageId = sessionManager.appendMessage(messageBody);
324
+ return { ok: true, messageId, message: messageBody };
87
325
  }
88
326
  catch (err) {
89
327
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
90
328
  }
91
- return { ok: true, messageId, message: transcriptEntry.message };
329
+ }
330
+ function collectSessionAbortPartials(params) {
331
+ const out = [];
332
+ for (const [runId, active] of params.chatAbortControllers) {
333
+ if (active.sessionKey !== params.sessionKey) {
334
+ continue;
335
+ }
336
+ const text = params.chatRunBuffers.get(runId);
337
+ if (!text || !text.trim()) {
338
+ continue;
339
+ }
340
+ out.push({
341
+ runId,
342
+ sessionId: active.sessionId,
343
+ text,
344
+ abortOrigin: params.abortOrigin,
345
+ });
346
+ }
347
+ return out;
348
+ }
349
+ function persistAbortedPartials(params) {
350
+ if (params.snapshots.length === 0) {
351
+ return;
352
+ }
353
+ const { storePath, entry } = loadSessionEntry(params.sessionKey);
354
+ for (const snapshot of params.snapshots) {
355
+ const sessionId = entry?.sessionId ?? snapshot.sessionId ?? snapshot.runId;
356
+ const appended = appendAssistantTranscriptMessage({
357
+ message: snapshot.text,
358
+ sessionId,
359
+ storePath,
360
+ sessionFile: entry?.sessionFile,
361
+ createIfMissing: true,
362
+ idempotencyKey: `${snapshot.runId}:assistant`,
363
+ abortMeta: {
364
+ aborted: true,
365
+ origin: snapshot.abortOrigin,
366
+ runId: snapshot.runId,
367
+ },
368
+ });
369
+ if (!appended.ok) {
370
+ params.context.logGateway.warn(`chat.abort transcript append failed: ${appended.error ?? "unknown error"}`);
371
+ }
372
+ }
373
+ }
374
+ function createChatAbortOps(context) {
375
+ return {
376
+ chatAbortControllers: context.chatAbortControllers,
377
+ chatRunBuffers: context.chatRunBuffers,
378
+ chatDeltaSentAt: context.chatDeltaSentAt,
379
+ chatAbortedRuns: context.chatAbortedRuns,
380
+ removeChatRun: context.removeChatRun,
381
+ agentRunSeq: context.agentRunSeq,
382
+ broadcast: context.broadcast,
383
+ nodeSendToSession: context.nodeSendToSession,
384
+ };
385
+ }
386
+ function abortChatRunsForSessionKeyWithPartials(params) {
387
+ const snapshots = collectSessionAbortPartials({
388
+ chatAbortControllers: params.context.chatAbortControllers,
389
+ chatRunBuffers: params.context.chatRunBuffers,
390
+ sessionKey: params.sessionKey,
391
+ abortOrigin: params.abortOrigin,
392
+ });
393
+ const res = abortChatRunsForSessionKey(params.ops, {
394
+ sessionKey: params.sessionKey,
395
+ stopReason: params.stopReason,
396
+ });
397
+ if (res.aborted) {
398
+ persistAbortedPartials({
399
+ context: params.context,
400
+ sessionKey: params.sessionKey,
401
+ snapshots,
402
+ });
403
+ }
404
+ return res;
92
405
  }
93
406
  function nextChatSeq(context, runId) {
94
407
  const next = (context.agentRunSeq.get(runId) ?? 0) + 1;
@@ -106,6 +419,7 @@ function broadcastChatFinal(params) {
106
419
  };
107
420
  params.context.broadcast("chat", payload);
108
421
  params.context.nodeSendToSession(params.sessionKey, "chat", payload);
422
+ params.context.agentRunSeq.delete(params.runId);
109
423
  }
110
424
  function broadcastChatError(params) {
111
425
  const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
@@ -118,6 +432,7 @@ function broadcastChatError(params) {
118
432
  };
119
433
  params.context.broadcast("chat", payload);
120
434
  params.context.nodeSendToSession(params.sessionKey, "chat", payload);
435
+ params.context.agentRunSeq.delete(params.runId);
121
436
  }
122
437
  export const chatHandlers = {
123
438
  "chat.history": async ({ params, respond, context }) => {
@@ -135,7 +450,20 @@ export const chatHandlers = {
135
450
  const max = Math.min(hardMax, requested);
136
451
  const sliced = rawMessages.length > max ? rawMessages.slice(-max) : rawMessages;
137
452
  const sanitized = stripEnvelopeFromMessages(sliced);
138
- const capped = capArrayByJsonBytes(sanitized, getMaxChatHistoryMessagesBytes()).items;
453
+ const normalized = sanitizeChatHistoryMessages(sanitized);
454
+ const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
455
+ const perMessageHardCap = Math.min(CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, maxHistoryBytes);
456
+ const replaced = replaceOversizedChatHistoryMessages({
457
+ messages: normalized,
458
+ maxSingleMessageBytes: perMessageHardCap,
459
+ });
460
+ const capped = capArrayByJsonBytes(replaced.messages, maxHistoryBytes).items;
461
+ const bounded = enforceChatHistoryFinalBudget({ messages: capped, maxBytes: maxHistoryBytes });
462
+ const placeholderCount = replaced.replacedCount + bounded.placeholderCount;
463
+ if (placeholderCount > 0) {
464
+ chatHistoryPlaceholderEmitCount += placeholderCount;
465
+ context.logGateway.debug(`chat.history omitted oversized payloads placeholders=${placeholderCount} total=${chatHistoryPlaceholderEmitCount}`);
466
+ }
139
467
  let thinkingLevel = entry?.thinkingLevel;
140
468
  if (!thinkingLevel) {
141
469
  const configured = cfg.agents?.defaults?.thinkingDefault;
@@ -143,7 +471,8 @@ export const chatHandlers = {
143
471
  thinkingLevel = configured;
144
472
  }
145
473
  else {
146
- const { provider, model } = resolveSessionModelRef(cfg, entry);
474
+ const sessionAgentId = resolveSessionAgentId({ sessionKey, config: cfg });
475
+ const { provider, model } = resolveSessionModelRef(cfg, entry, sessionAgentId);
147
476
  const catalog = await context.loadGatewayModelCatalog();
148
477
  thinkingLevel = resolveThinkingDefault({
149
478
  cfg,
@@ -153,11 +482,13 @@ export const chatHandlers = {
153
482
  });
154
483
  }
155
484
  }
485
+ const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault;
156
486
  respond(true, {
157
487
  sessionKey,
158
488
  sessionId,
159
- messages: capped,
489
+ messages: bounded.messages,
160
490
  thinkingLevel,
491
+ verboseLevel,
161
492
  });
162
493
  },
163
494
  "chat.abort": ({ params, respond, context }) => {
@@ -165,20 +496,14 @@ export const chatHandlers = {
165
496
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.abort params: ${formatValidationErrors(validateChatAbortParams.errors)}`));
166
497
  return;
167
498
  }
168
- const { sessionKey, runId } = params;
169
- const ops = {
170
- chatAbortControllers: context.chatAbortControllers,
171
- chatRunBuffers: context.chatRunBuffers,
172
- chatDeltaSentAt: context.chatDeltaSentAt,
173
- chatAbortedRuns: context.chatAbortedRuns,
174
- removeChatRun: context.removeChatRun,
175
- agentRunSeq: context.agentRunSeq,
176
- broadcast: context.broadcast,
177
- nodeSendToSession: context.nodeSendToSession,
178
- };
499
+ const { sessionKey: rawSessionKey, runId } = params;
500
+ const ops = createChatAbortOps(context);
179
501
  if (!runId) {
180
- const res = abortChatRunsForSessionKey(ops, {
181
- sessionKey,
502
+ const res = abortChatRunsForSessionKeyWithPartials({
503
+ context,
504
+ ops,
505
+ sessionKey: rawSessionKey,
506
+ abortOrigin: "rpc",
182
507
  stopReason: "rpc",
183
508
  });
184
509
  respond(true, { ok: true, aborted: res.aborted, runIds: res.runIds });
@@ -189,15 +514,30 @@ export const chatHandlers = {
189
514
  respond(true, { ok: true, aborted: false, runIds: [] });
190
515
  return;
191
516
  }
192
- if (active.sessionKey !== sessionKey) {
517
+ if (active.sessionKey !== rawSessionKey) {
193
518
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "runId does not match sessionKey"));
194
519
  return;
195
520
  }
521
+ const partialText = context.chatRunBuffers.get(runId);
196
522
  const res = abortChatRunById(ops, {
197
523
  runId,
198
- sessionKey,
524
+ sessionKey: rawSessionKey,
199
525
  stopReason: "rpc",
200
526
  });
527
+ if (res.aborted && partialText && partialText.trim()) {
528
+ persistAbortedPartials({
529
+ context,
530
+ sessionKey: rawSessionKey,
531
+ snapshots: [
532
+ {
533
+ runId,
534
+ sessionId: active.sessionId,
535
+ text: partialText,
536
+ abortOrigin: "rpc",
537
+ },
538
+ ],
539
+ });
540
+ }
201
541
  respond(true, {
202
542
  ok: true,
203
543
  aborted: res.aborted,
@@ -210,29 +550,24 @@ export const chatHandlers = {
210
550
  return;
211
551
  }
212
552
  const p = params;
213
- const stopCommand = isChatStopCommandText(p.message);
214
- const normalizedAttachments = p.attachments
215
- ?.map((a) => ({
216
- type: typeof a?.type === "string" ? a.type : undefined,
217
- mimeType: typeof a?.mimeType === "string" ? a.mimeType : undefined,
218
- fileName: typeof a?.fileName === "string" ? a.fileName : undefined,
219
- content: typeof a?.content === "string"
220
- ? a.content
221
- : ArrayBuffer.isView(a?.content)
222
- ? Buffer.from(a.content.buffer, a.content.byteOffset, a.content.byteLength).toString("base64")
223
- : undefined,
224
- }))
225
- .filter((a) => a.content) ?? [];
226
- const rawMessage = p.message.trim();
553
+ const sanitizedMessageResult = sanitizeChatSendMessageInput(p.message);
554
+ if (!sanitizedMessageResult.ok) {
555
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, sanitizedMessageResult.error));
556
+ return;
557
+ }
558
+ const inboundMessage = sanitizedMessageResult.message;
559
+ const stopCommand = isChatStopCommandText(inboundMessage);
560
+ const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments);
561
+ const rawMessage = inboundMessage.trim();
227
562
  if (!rawMessage && normalizedAttachments.length === 0) {
228
563
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "message or attachment required"));
229
564
  return;
230
565
  }
231
- let parsedMessage = p.message;
566
+ let parsedMessage = inboundMessage;
232
567
  let parsedImages = [];
233
568
  if (normalizedAttachments.length > 0) {
234
569
  try {
235
- const parsed = await parseMessageWithAttachments(p.message, normalizedAttachments, {
570
+ const parsed = await parseMessageWithAttachments(inboundMessage, normalizedAttachments, {
236
571
  maxBytes: 5_000_000,
237
572
  log: context.logGateway,
238
573
  });
@@ -244,7 +579,8 @@ export const chatHandlers = {
244
579
  return;
245
580
  }
246
581
  }
247
- const { cfg, entry } = loadSessionEntry(p.sessionKey);
582
+ const rawSessionKey = p.sessionKey;
583
+ const { cfg, entry, canonicalKey: sessionKey } = loadSessionEntry(rawSessionKey);
248
584
  const timeoutMs = resolveAgentTimeoutMs({
249
585
  cfg,
250
586
  overrideMs: p.timeoutMs,
@@ -254,7 +590,7 @@ export const chatHandlers = {
254
590
  const sendPolicy = resolveSendPolicy({
255
591
  cfg,
256
592
  entry,
257
- sessionKey: p.sessionKey,
593
+ sessionKey,
258
594
  channel: entry?.channel,
259
595
  chatType: entry?.chatType,
260
596
  });
@@ -263,16 +599,13 @@ export const chatHandlers = {
263
599
  return;
264
600
  }
265
601
  if (stopCommand) {
266
- const res = abortChatRunsForSessionKey({
267
- chatAbortControllers: context.chatAbortControllers,
268
- chatRunBuffers: context.chatRunBuffers,
269
- chatDeltaSentAt: context.chatDeltaSentAt,
270
- chatAbortedRuns: context.chatAbortedRuns,
271
- removeChatRun: context.removeChatRun,
272
- agentRunSeq: context.agentRunSeq,
273
- broadcast: context.broadcast,
274
- nodeSendToSession: context.nodeSendToSession,
275
- }, { sessionKey: p.sessionKey, stopReason: "stop" });
602
+ const res = abortChatRunsForSessionKeyWithPartials({
603
+ context,
604
+ ops: createChatAbortOps(context),
605
+ sessionKey: rawSessionKey,
606
+ abortOrigin: "stop-command",
607
+ stopReason: "stop",
608
+ });
276
609
  respond(true, { ok: true, aborted: res.aborted, runIds: res.runIds });
277
610
  return;
278
611
  }
@@ -296,7 +629,7 @@ export const chatHandlers = {
296
629
  context.chatAbortControllers.set(clientRunId, {
297
630
  controller: abortController,
298
631
  sessionId: entry?.sessionId ?? clientRunId,
299
- sessionKey: p.sessionKey,
632
+ sessionKey: rawSessionKey,
300
633
  startedAtMs: now,
301
634
  expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
302
635
  });
@@ -309,13 +642,17 @@ export const chatHandlers = {
309
642
  const injectThinking = Boolean(p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"));
310
643
  const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
311
644
  const clientInfo = client?.connect?.client;
645
+ // Inject timestamp so agents know the current date/time.
646
+ // Only BodyForAgent gets the timestamp — Body stays raw for UI display.
647
+ // See: https://github.com/moltbot/moltbot/issues/3658
648
+ const stampedMessage = injectTimestamp(parsedMessage, timestampOptsFromConfig(cfg));
312
649
  const ctx = {
313
650
  Body: parsedMessage,
314
- BodyForAgent: parsedMessage,
651
+ BodyForAgent: stampedMessage,
315
652
  BodyForCommands: commandBody,
316
653
  RawBody: parsedMessage,
317
654
  CommandBody: commandBody,
318
- SessionKey: p.sessionKey,
655
+ SessionKey: sessionKey,
319
656
  Provider: INTERNAL_MESSAGE_CHANNEL,
320
657
  Surface: INTERNAL_MESSAGE_CHANNEL,
321
658
  OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
@@ -325,27 +662,31 @@ export const chatHandlers = {
325
662
  SenderId: clientInfo?.id,
326
663
  SenderName: clientInfo?.displayName,
327
664
  SenderUsername: clientInfo?.displayName,
665
+ GatewayClientScopes: client?.connect?.scopes,
328
666
  };
329
667
  const agentId = resolveSessionAgentId({
330
- sessionKey: p.sessionKey,
668
+ sessionKey,
331
669
  config: cfg,
332
670
  });
333
- let prefixContext = {
334
- identityName: resolveIdentityName(cfg, agentId),
335
- };
671
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
672
+ cfg,
673
+ agentId,
674
+ channel: INTERNAL_MESSAGE_CHANNEL,
675
+ });
336
676
  const finalReplyParts = [];
337
677
  const dispatcher = createReplyDispatcher({
338
- responsePrefix: resolveEffectiveMessagesConfig(cfg, agentId).responsePrefix,
339
- responsePrefixContextProvider: () => prefixContext,
678
+ ...prefixOptions,
340
679
  onError: (err) => {
341
680
  context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
342
681
  },
343
682
  deliver: async (payload, info) => {
344
- if (info.kind !== "final")
683
+ if (info.kind !== "final") {
345
684
  return;
685
+ }
346
686
  const text = payload.text?.trim() ?? "";
347
- if (!text)
687
+ if (!text) {
348
688
  return;
689
+ }
349
690
  finalReplyParts.push(text);
350
691
  },
351
692
  });
@@ -358,16 +699,23 @@ export const chatHandlers = {
358
699
  runId: clientRunId,
359
700
  abortSignal: abortController.signal,
360
701
  images: parsedImages.length > 0 ? parsedImages : undefined,
361
- disableBlockStreaming: true,
362
- onAgentRunStart: () => {
702
+ onAgentRunStart: (runId) => {
363
703
  agentRunStarted = true;
704
+ const connId = typeof client?.connId === "string" ? client.connId : undefined;
705
+ const wantsToolEvents = hasGatewayClientCap(client?.connect?.caps, GATEWAY_CLIENT_CAPS.TOOL_EVENTS);
706
+ if (connId && wantsToolEvents) {
707
+ context.registerToolEventRecipient(runId, connId);
708
+ // Register for any other active runs *in the same session* so
709
+ // late-joining clients (e.g. page refresh mid-response) receive
710
+ // in-progress tool events without leaking cross-session data.
711
+ for (const [activeRunId, active] of context.chatAbortControllers) {
712
+ if (activeRunId !== runId && active.sessionKey === p.sessionKey) {
713
+ context.registerToolEventRecipient(activeRunId, connId);
714
+ }
715
+ }
716
+ }
364
717
  },
365
- onModelSelected: (ctx) => {
366
- prefixContext.provider = ctx.provider;
367
- prefixContext.model = extractShortModelName(ctx.model);
368
- prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
369
- prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
370
- },
718
+ onModelSelected,
371
719
  },
372
720
  })
373
721
  .then(() => {
@@ -379,13 +727,14 @@ export const chatHandlers = {
379
727
  .trim();
380
728
  let message;
381
729
  if (combinedReply) {
382
- const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(p.sessionKey);
730
+ const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey);
383
731
  const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId;
384
732
  const appended = appendAssistantTranscriptMessage({
385
733
  message: combinedReply,
386
734
  sessionId,
387
735
  storePath: latestStorePath,
388
736
  sessionFile: latestEntry?.sessionFile,
737
+ agentId,
389
738
  createIfMissing: true,
390
739
  });
391
740
  if (appended.ok) {
@@ -398,7 +747,9 @@ export const chatHandlers = {
398
747
  role: "assistant",
399
748
  content: [{ type: "text", text: combinedReply }],
400
749
  timestamp: now,
401
- stopReason: "injected",
750
+ // Keep this compatible with Pi stopReason enums even though this message isn't
751
+ // persisted to the transcript due to the append failure.
752
+ stopReason: "stop",
402
753
  usage: { input: 0, output: 0, totalTokens: 0 },
403
754
  };
404
755
  }
@@ -406,7 +757,7 @@ export const chatHandlers = {
406
757
  broadcastChatFinal({
407
758
  context,
408
759
  runId: clientRunId,
409
- sessionKey: p.sessionKey,
760
+ sessionKey: rawSessionKey,
410
761
  message,
411
762
  });
412
763
  }
@@ -431,7 +782,7 @@ export const chatHandlers = {
431
782
  broadcastChatError({
432
783
  context,
433
784
  runId: clientRunId,
434
- sessionKey: p.sessionKey,
785
+ sessionKey: rawSessionKey,
435
786
  errorMessage: String(err),
436
787
  });
437
788
  })
@@ -465,56 +816,36 @@ export const chatHandlers = {
465
816
  }
466
817
  const p = params;
467
818
  // Load session to find transcript file
468
- const { storePath, entry } = loadSessionEntry(p.sessionKey);
819
+ const rawSessionKey = p.sessionKey;
820
+ const { cfg, storePath, entry } = loadSessionEntry(rawSessionKey);
469
821
  const sessionId = entry?.sessionId;
470
822
  if (!sessionId || !storePath) {
471
823
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "session not found"));
472
824
  return;
473
825
  }
474
- // Resolve transcript path
475
- const transcriptPath = entry?.sessionFile
476
- ? entry.sessionFile
477
- : path.join(path.dirname(storePath), `${sessionId}.jsonl`);
478
- if (!fs.existsSync(transcriptPath)) {
479
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "transcript file not found"));
480
- return;
481
- }
482
- // Build transcript entry
483
- const now = Date.now();
484
- const messageId = randomUUID().slice(0, 8);
485
- const labelPrefix = p.label ? `[${p.label}]\n\n` : "";
486
- const messageBody = {
487
- role: "assistant",
488
- content: [{ type: "text", text: `${labelPrefix}${p.message}` }],
489
- timestamp: now,
490
- stopReason: "injected",
491
- usage: { input: 0, output: 0, totalTokens: 0 },
492
- };
493
- const transcriptEntry = {
494
- type: "message",
495
- id: messageId,
496
- timestamp: new Date(now).toISOString(),
497
- message: messageBody,
498
- };
499
- // Append to transcript file
500
- try {
501
- fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8");
502
- }
503
- catch (err) {
504
- const errMessage = err instanceof Error ? err.message : String(err);
505
- respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${errMessage}`));
826
+ const appended = appendAssistantTranscriptMessage({
827
+ message: p.message,
828
+ label: p.label,
829
+ sessionId,
830
+ storePath,
831
+ sessionFile: entry?.sessionFile,
832
+ agentId: resolveSessionAgentId({ sessionKey: rawSessionKey, config: cfg }),
833
+ createIfMissing: false,
834
+ });
835
+ if (!appended.ok || !appended.messageId || !appended.message) {
836
+ respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${appended.error ?? "unknown error"}`));
506
837
  return;
507
838
  }
508
839
  // Broadcast to webchat for immediate UI update
509
840
  const chatPayload = {
510
- runId: `inject-${messageId}`,
511
- sessionKey: p.sessionKey,
841
+ runId: `inject-${appended.messageId}`,
842
+ sessionKey: rawSessionKey,
512
843
  seq: 0,
513
844
  state: "final",
514
- message: transcriptEntry.message,
845
+ message: appended.message,
515
846
  };
516
847
  context.broadcast("chat", chatPayload);
517
- context.nodeSendToSession(p.sessionKey, "chat", chatPayload);
518
- respond(true, { ok: true, messageId });
848
+ context.nodeSendToSession(rawSessionKey, "chat", chatPayload);
849
+ respond(true, { ok: true, messageId: appended.messageId });
519
850
  },
520
851
  };