@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -6,7 +6,8 @@ import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-span
6
6
  import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
7
7
  import { isMessagingToolDuplicateNormalized, normalizeTextForComparison, } from "./pi-embedded-helpers.js";
8
8
  import { createEmbeddedPiSessionEventHandler } from "./pi-embedded-subscribe.handlers.js";
9
- import { formatReasoningMessage } from "./pi-embedded-utils.js";
9
+ import { formatReasoningMessage, stripDowngradedToolCallText } from "./pi-embedded-utils.js";
10
+ import { hasNonzeroUsage, normalizeUsage } from "./usage.js";
10
11
  const THINKING_TAG_SCAN_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
11
12
  const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi;
12
13
  const log = createSubsystemLogger("agent/embedded");
@@ -29,7 +30,10 @@ export function subscribeEmbeddedPiSession(params) {
29
30
  blockBuffer: "",
30
31
  // Track if a streamed chunk opened a <think> block (stateful across chunks).
31
32
  blockState: { thinking: false, final: false, inlineCode: createInlineCodeState() },
33
+ partialBlockState: { thinking: false, final: false, inlineCode: createInlineCodeState() },
32
34
  lastStreamedAssistant: undefined,
35
+ lastStreamedAssistantCleaned: undefined,
36
+ emittedAssistantUpdate: false,
33
37
  lastStreamedReasoning: undefined,
34
38
  lastBlockReplyText: undefined,
35
39
  assistantMessageIndex: 0,
@@ -49,6 +53,14 @@ export function subscribeEmbeddedPiSession(params) {
49
53
  pendingMessagingTexts: new Map(),
50
54
  pendingMessagingTargets: new Map(),
51
55
  };
56
+ const usageTotals = {
57
+ input: 0,
58
+ output: 0,
59
+ cacheRead: 0,
60
+ cacheWrite: 0,
61
+ total: 0,
62
+ };
63
+ let compactionCount = 0;
52
64
  const assistantTexts = state.assistantTexts;
53
65
  const toolMetas = state.toolMetas;
54
66
  const toolMetaById = state.toolMetaById;
@@ -59,15 +71,22 @@ export function subscribeEmbeddedPiSession(params) {
59
71
  const pendingMessagingTexts = state.pendingMessagingTexts;
60
72
  const pendingMessagingTargets = state.pendingMessagingTargets;
61
73
  const replyDirectiveAccumulator = createStreamingDirectiveAccumulator();
74
+ const partialReplyDirectiveAccumulator = createStreamingDirectiveAccumulator();
62
75
  const resetAssistantMessageState = (nextAssistantTextBaseline) => {
63
76
  state.deltaBuffer = "";
64
77
  state.blockBuffer = "";
65
78
  blockChunker?.reset();
66
79
  replyDirectiveAccumulator.reset();
80
+ partialReplyDirectiveAccumulator.reset();
67
81
  state.blockState.thinking = false;
68
82
  state.blockState.final = false;
69
83
  state.blockState.inlineCode = createInlineCodeState();
84
+ state.partialBlockState.thinking = false;
85
+ state.partialBlockState.final = false;
86
+ state.partialBlockState.inlineCode = createInlineCodeState();
70
87
  state.lastStreamedAssistant = undefined;
88
+ state.lastStreamedAssistantCleaned = undefined;
89
+ state.emittedAssistantUpdate = false;
71
90
  state.lastBlockReplyText = undefined;
72
91
  state.lastStreamedReasoning = undefined;
73
92
  state.lastReasoningSent = undefined;
@@ -85,21 +104,26 @@ export function subscribeEmbeddedPiSession(params) {
85
104
  state.lastAssistantTextNormalized = normalized.length > 0 ? normalized : undefined;
86
105
  };
87
106
  const shouldSkipAssistantText = (text) => {
88
- if (state.lastAssistantTextMessageIndex !== state.assistantMessageIndex)
107
+ if (state.lastAssistantTextMessageIndex !== state.assistantMessageIndex) {
89
108
  return false;
109
+ }
90
110
  const trimmed = text.trimEnd();
91
- if (trimmed && trimmed === state.lastAssistantTextTrimmed)
111
+ if (trimmed && trimmed === state.lastAssistantTextTrimmed) {
92
112
  return true;
113
+ }
93
114
  const normalized = normalizeTextForComparison(text);
94
- if (normalized.length > 0 && normalized === state.lastAssistantTextNormalized)
115
+ if (normalized.length > 0 && normalized === state.lastAssistantTextNormalized) {
95
116
  return true;
117
+ }
96
118
  return false;
97
119
  };
98
120
  const pushAssistantText = (text) => {
99
- if (!text)
121
+ if (!text) {
100
122
  return;
101
- if (shouldSkipAssistantText(text))
123
+ }
124
+ if (shouldSkipAssistantText(text)) {
102
125
  return;
126
+ }
103
127
  assistantTexts.push(text);
104
128
  rememberAssistantText(text);
105
129
  };
@@ -154,8 +178,9 @@ export function subscribeEmbeddedPiSession(params) {
154
178
  ensureCompactionPromise();
155
179
  };
156
180
  const resolveCompactionRetry = () => {
157
- if (state.pendingCompactionRetry <= 0)
181
+ if (state.pendingCompactionRetry <= 0) {
158
182
  return;
183
+ }
159
184
  state.pendingCompactionRetry -= 1;
160
185
  if (state.pendingCompactionRetry === 0 && !state.compactionInFlight) {
161
186
  state.compactionRetryResolve?.();
@@ -170,6 +195,40 @@ export function subscribeEmbeddedPiSession(params) {
170
195
  state.compactionRetryPromise = null;
171
196
  }
172
197
  };
198
+ const recordAssistantUsage = (usageLike) => {
199
+ const usage = normalizeUsage((usageLike ?? undefined));
200
+ if (!hasNonzeroUsage(usage)) {
201
+ return;
202
+ }
203
+ usageTotals.input += usage.input ?? 0;
204
+ usageTotals.output += usage.output ?? 0;
205
+ usageTotals.cacheRead += usage.cacheRead ?? 0;
206
+ usageTotals.cacheWrite += usage.cacheWrite ?? 0;
207
+ const usageTotal = usage.total ??
208
+ (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
209
+ usageTotals.total += usageTotal;
210
+ };
211
+ const getUsageTotals = () => {
212
+ const hasUsage = usageTotals.input > 0 ||
213
+ usageTotals.output > 0 ||
214
+ usageTotals.cacheRead > 0 ||
215
+ usageTotals.cacheWrite > 0 ||
216
+ usageTotals.total > 0;
217
+ if (!hasUsage) {
218
+ return undefined;
219
+ }
220
+ const derivedTotal = usageTotals.input + usageTotals.output + usageTotals.cacheRead + usageTotals.cacheWrite;
221
+ return {
222
+ input: usageTotals.input || undefined,
223
+ output: usageTotals.output || undefined,
224
+ cacheRead: usageTotals.cacheRead || undefined,
225
+ cacheWrite: usageTotals.cacheWrite || undefined,
226
+ total: usageTotals.total || derivedTotal || undefined,
227
+ };
228
+ };
229
+ const incrementCompactionCount = () => {
230
+ compactionCount += 1;
231
+ };
173
232
  const blockChunking = params.blockReplyChunking;
174
233
  const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null;
175
234
  // KNOWN: Provider streams are not strictly once-only or perfectly ordered.
@@ -183,21 +242,25 @@ export function subscribeEmbeddedPiSession(params) {
183
242
  : params.verboseLevel === "full";
184
243
  const formatToolOutputBlock = (text) => {
185
244
  const trimmed = text.trim();
186
- if (!trimmed)
245
+ if (!trimmed) {
187
246
  return "(no output)";
188
- if (!useMarkdown)
247
+ }
248
+ if (!useMarkdown) {
189
249
  return trimmed;
250
+ }
190
251
  return `\`\`\`txt\n${trimmed}\n\`\`\``;
191
252
  };
192
253
  const emitToolSummary = (toolName, meta) => {
193
- if (!params.onToolResult)
254
+ if (!params.onToolResult) {
194
255
  return;
256
+ }
195
257
  const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
196
258
  markdown: useMarkdown,
197
259
  });
198
260
  const { text: cleanedText, mediaUrls } = parseReplyDirectives(agg);
199
- if (!cleanedText && (!mediaUrls || mediaUrls.length === 0))
261
+ if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) {
200
262
  return;
263
+ }
201
264
  try {
202
265
  void params.onToolResult({
203
266
  text: cleanedText,
@@ -209,15 +272,17 @@ export function subscribeEmbeddedPiSession(params) {
209
272
  }
210
273
  };
211
274
  const emitToolOutput = (toolName, meta, output) => {
212
- if (!params.onToolResult || !output)
275
+ if (!params.onToolResult || !output) {
213
276
  return;
277
+ }
214
278
  const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
215
279
  markdown: useMarkdown,
216
280
  });
217
281
  const message = `${agg}\n${formatToolOutputBlock(output)}`;
218
282
  const { text: cleanedText, mediaUrls } = parseReplyDirectives(message);
219
- if (!cleanedText && (!mediaUrls || mediaUrls.length === 0))
283
+ if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) {
220
284
  return;
285
+ }
221
286
  try {
222
287
  void params.onToolResult({
223
288
  text: cleanedText,
@@ -229,8 +294,9 @@ export function subscribeEmbeddedPiSession(params) {
229
294
  }
230
295
  };
231
296
  const stripBlockTags = (text, state) => {
232
- if (!text)
297
+ if (!text) {
233
298
  return text;
299
+ }
234
300
  const inlineStateStart = state.inlineCode ?? createInlineCodeState();
235
301
  const codeSpans = buildCodeSpanIndex(text, inlineStateStart);
236
302
  // 1. Handle <think> blocks (stateful, strip content inside)
@@ -240,8 +306,9 @@ export function subscribeEmbeddedPiSession(params) {
240
306
  let inThinking = state.thinking;
241
307
  for (const match of text.matchAll(THINKING_TAG_SCAN_RE)) {
242
308
  const idx = match.index ?? 0;
243
- if (codeSpans.isInside(idx))
309
+ if (codeSpans.isInside(idx)) {
244
310
  continue;
311
+ }
245
312
  if (!inThinking) {
246
313
  processed += text.slice(lastIndex, idx);
247
314
  }
@@ -271,8 +338,9 @@ export function subscribeEmbeddedPiSession(params) {
271
338
  let everInFinal = state.final;
272
339
  for (const match of processed.matchAll(FINAL_TAG_SCAN_RE)) {
273
340
  const idx = match.index ?? 0;
274
- if (finalCodeSpans.isInside(idx))
341
+ if (finalCodeSpans.isInside(idx)) {
275
342
  continue;
343
+ }
276
344
  const isClose = match[1] === "/";
277
345
  if (!inFinal && !isClose) {
278
346
  // Found <final> start tag.
@@ -309,8 +377,9 @@ export function subscribeEmbeddedPiSession(params) {
309
377
  pattern.lastIndex = 0;
310
378
  for (const match of text.matchAll(pattern)) {
311
379
  const idx = match.index ?? 0;
312
- if (isInside(idx))
380
+ if (isInside(idx)) {
313
381
  continue;
382
+ }
314
383
  output += text.slice(lastIndex, idx);
315
384
  lastIndex = idx + match[0].length;
316
385
  }
@@ -318,14 +387,18 @@ export function subscribeEmbeddedPiSession(params) {
318
387
  return output;
319
388
  };
320
389
  const emitBlockChunk = (text) => {
321
- if (state.suppressBlockChunks)
390
+ if (state.suppressBlockChunks) {
322
391
  return;
392
+ }
323
393
  // Strip <think> and <final> blocks across chunk boundaries to avoid leaking reasoning.
324
- const chunk = stripBlockTags(text, state.blockState).trimEnd();
325
- if (!chunk)
394
+ // Also strip downgraded tool call text ([Tool Call: ...], [Historical context: ...], etc.).
395
+ const chunk = stripDowngradedToolCallText(stripBlockTags(text, state.blockState)).trimEnd();
396
+ if (!chunk) {
326
397
  return;
327
- if (chunk === state.lastBlockReplyText)
398
+ }
399
+ if (chunk === state.lastBlockReplyText) {
328
400
  return;
401
+ }
329
402
  // Only check committed (successful) messaging tool texts - checking pending texts
330
403
  // is risky because if the tool fails after suppression, the user gets no response
331
404
  const normalizedChunk = normalizeTextForComparison(chunk);
@@ -333,20 +406,24 @@ export function subscribeEmbeddedPiSession(params) {
333
406
  log.debug(`Skipping block reply - already sent via messaging tool: ${chunk.slice(0, 50)}...`);
334
407
  return;
335
408
  }
336
- if (shouldSkipAssistantText(chunk))
409
+ if (shouldSkipAssistantText(chunk)) {
337
410
  return;
411
+ }
338
412
  state.lastBlockReplyText = chunk;
339
413
  assistantTexts.push(chunk);
340
414
  rememberAssistantText(chunk);
341
- if (!params.onBlockReply)
415
+ if (!params.onBlockReply) {
342
416
  return;
417
+ }
343
418
  const splitResult = replyDirectiveAccumulator.consume(chunk);
344
- if (!splitResult)
419
+ if (!splitResult) {
345
420
  return;
421
+ }
346
422
  const { text: cleanedText, mediaUrls, audioAsVoice, replyToId, replyToTag, replyToCurrent, } = splitResult;
347
423
  // Skip empty payloads, but always emit if audioAsVoice is set (to propagate the flag)
348
- if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice)
424
+ if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice) {
349
425
  return;
426
+ }
350
427
  void params.onBlockReply({
351
428
  text: cleanedText,
352
429
  mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
@@ -357,9 +434,11 @@ export function subscribeEmbeddedPiSession(params) {
357
434
  });
358
435
  };
359
436
  const consumeReplyDirectives = (text, options) => replyDirectiveAccumulator.consume(text, options);
437
+ const consumePartialReplyDirectives = (text, options) => partialReplyDirectiveAccumulator.consume(text, options);
360
438
  const flushBlockReplyBuffer = () => {
361
- if (!params.onBlockReply)
439
+ if (!params.onBlockReply) {
362
440
  return;
441
+ }
363
442
  if (blockChunker?.hasBuffered()) {
364
443
  blockChunker.drain({ force: true, emit: emitBlockChunk });
365
444
  blockChunker.reset();
@@ -371,13 +450,16 @@ export function subscribeEmbeddedPiSession(params) {
371
450
  }
372
451
  };
373
452
  const emitReasoningStream = (text) => {
374
- if (!state.streamReasoning || !params.onReasoningStream)
453
+ if (!state.streamReasoning || !params.onReasoningStream) {
375
454
  return;
455
+ }
376
456
  const formatted = formatReasoningMessage(text);
377
- if (!formatted)
457
+ if (!formatted) {
378
458
  return;
379
- if (formatted === state.lastStreamedReasoning)
459
+ }
460
+ if (formatted === state.lastStreamedReasoning) {
380
461
  return;
462
+ }
381
463
  state.lastStreamedReasoning = formatted;
382
464
  void params.onReasoningStream({
383
465
  text: formatted,
@@ -411,6 +493,7 @@ export function subscribeEmbeddedPiSession(params) {
411
493
  flushBlockReplyBuffer,
412
494
  emitReasoningStream,
413
495
  consumeReplyDirectives,
496
+ consumePartialReplyDirectives,
414
497
  resetAssistantMessageState,
415
498
  resetForCompactionRetry,
416
499
  finalizeAssistantTexts,
@@ -419,6 +502,10 @@ export function subscribeEmbeddedPiSession(params) {
419
502
  noteCompactionRetry,
420
503
  resolveCompactionRetry,
421
504
  maybeResolveCompactionWait,
505
+ recordAssistantUsage,
506
+ incrementCompactionCount,
507
+ getUsageTotals,
508
+ getCompactionCount: () => compactionCount,
422
509
  };
423
510
  const unsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx));
424
511
  return {
@@ -433,6 +520,8 @@ export function subscribeEmbeddedPiSession(params) {
433
520
  // which is generated AFTER the tool sends the actual answer.
434
521
  didSendViaMessagingTool: () => messagingToolSentTexts.length > 0,
435
522
  getLastToolError: () => (state.lastToolError ? { ...state.lastToolError } : undefined),
523
+ getUsageTotals,
524
+ getCompactionCount: () => compactionCount,
436
525
  waitForCompactionRetry: () => {
437
526
  if (state.compactionInFlight || state.pendingCompactionRetry > 0) {
438
527
  ensureCompactionPromise();
@@ -10,7 +10,7 @@ import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
10
10
  import { filterToolsByPolicy, isToolAllowedByPolicies, resolveEffectiveToolPolicy, resolveGroupToolPolicy, resolveSubagentToolPolicy, } from "./pi-tools.policy.js";
11
11
  import { assertRequiredParams, CLAUDE_PARAM_GROUPS, createPoolbotReadTool, createSandboxedEditTool, createSandboxedReadTool, createSandboxedWriteTool, normalizeToolParams, patchToolSchemaForClaudeCompatibility, wrapToolParamNormalization, } from "./pi-tools.read.js";
12
12
  import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
13
- import { buildPluginToolGroups, collectExplicitAllowlist, expandPolicyWithPluginGroups, normalizeToolName, resolveToolProfilePolicy, stripPluginOnlyAllowlist, } from "./tool-policy.js";
13
+ import { buildPluginToolGroups, collectExplicitAllowlist, expandPolicyWithPluginGroups, normalizeToolName, resolveToolProfilePolicy, stripPluginOnlyAllowlist, applyOwnerOnlyToolPolicy, } from "./tool-policy.js";
14
14
  import { getPluginToolMeta } from "../plugins/tools.js";
15
15
  import { logWarn } from "../logger.js";
16
16
  function isOpenAIProvider(provider) {
@@ -227,15 +227,20 @@ export function createPoolbotCodingTools(options) {
227
227
  replyToMode: options?.replyToMode,
228
228
  hasRepliedRef: options?.hasRepliedRef,
229
229
  modelHasVision: options?.modelHasVision,
230
+ requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
231
+ disableMessageTool: options?.disableMessageTool,
230
232
  requesterAgentIdOverride: agentId,
231
233
  }),
232
234
  ];
233
- const coreToolNames = new Set(tools
235
+ // Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
236
+ const senderIsOwner = options?.senderIsOwner === true;
237
+ const toolsByAuthorization = applyOwnerOnlyToolPolicy(tools, senderIsOwner);
238
+ const coreToolNames = new Set(toolsByAuthorization
234
239
  .filter((tool) => !getPluginToolMeta(tool))
235
240
  .map((tool) => normalizeToolName(tool.name))
236
241
  .filter(Boolean));
237
242
  const pluginGroups = buildPluginToolGroups({
238
- tools,
243
+ tools: toolsByAuthorization,
239
244
  toolMeta: (tool) => getPluginToolMeta(tool),
240
245
  });
241
246
  const resolvePolicy = (policy, label) => {
@@ -259,8 +264,8 @@ export function createPoolbotCodingTools(options) {
259
264
  const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
260
265
  const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
261
266
  const toolsFiltered = profilePolicyExpanded
262
- ? filterToolsByPolicy(tools, profilePolicyExpanded)
263
- : tools;
267
+ ? filterToolsByPolicy(toolsByAuthorization, profilePolicyExpanded)
268
+ : toolsByAuthorization;
264
269
  const providerProfileFiltered = providerProfileExpanded
265
270
  ? filterToolsByPolicy(toolsFiltered, providerProfileExpanded)
266
271
  : toolsFiltered;
@@ -32,6 +32,20 @@ export function createPoolBotTools(options) {
32
32
  config: options?.config,
33
33
  sandboxed: options?.sandboxed,
34
34
  });
35
+ const messageTool = options?.disableMessageTool
36
+ ? null
37
+ : createMessageTool({
38
+ agentAccountId: options?.agentAccountId,
39
+ agentSessionKey: options?.agentSessionKey,
40
+ config: options?.config,
41
+ currentChannelId: options?.currentChannelId,
42
+ currentChannelProvider: options?.agentChannel,
43
+ currentThreadTs: options?.currentThreadTs,
44
+ replyToMode: options?.replyToMode,
45
+ hasRepliedRef: options?.hasRepliedRef,
46
+ sandboxRoot: options?.sandboxRoot,
47
+ requireExplicitTarget: options?.requireExplicitMessageTarget,
48
+ });
35
49
  const tools = [
36
50
  createBrowserTool({
37
51
  sandboxBridgeUrl: options?.sandboxBrowserBridgeUrl,
@@ -45,16 +59,7 @@ export function createPoolBotTools(options) {
45
59
  createCronTool({
46
60
  agentSessionKey: options?.agentSessionKey,
47
61
  }),
48
- createMessageTool({
49
- agentAccountId: options?.agentAccountId,
50
- agentSessionKey: options?.agentSessionKey,
51
- config: options?.config,
52
- currentChannelId: options?.currentChannelId,
53
- currentChannelProvider: options?.agentChannel,
54
- currentThreadTs: options?.currentThreadTs,
55
- replyToMode: options?.replyToMode,
56
- hasRepliedRef: options?.hasRepliedRef,
57
- }),
62
+ ...(messageTool ? [messageTool] : []),
58
63
  createTtsTool({
59
64
  agentChannel: options?.agentChannel,
60
65
  config: options?.config,
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const HTTP_URL_RE = /^https?:\/\//i;
6
+ const DATA_URL_RE = /^data:/i;
4
7
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
5
8
  function normalizeUnicodeSpaces(str) {
6
9
  return str.replace(UNICODE_SPACES, " ");
@@ -38,6 +41,34 @@ export async function assertSandboxPath(params) {
38
41
  await assertNoSymlink(resolved.relative, path.resolve(params.root));
39
42
  return resolved;
40
43
  }
44
+ export function assertMediaNotDataUrl(media) {
45
+ const raw = media.trim();
46
+ if (DATA_URL_RE.test(raw)) {
47
+ throw new Error("data: URLs are not supported for media. Use buffer instead.");
48
+ }
49
+ }
50
+ export async function resolveSandboxedMediaSource(params) {
51
+ const raw = params.media.trim();
52
+ if (!raw)
53
+ return raw;
54
+ if (HTTP_URL_RE.test(raw))
55
+ return raw;
56
+ let candidate = raw;
57
+ if (/^file:\/\//i.test(candidate)) {
58
+ try {
59
+ candidate = fileURLToPath(candidate);
60
+ }
61
+ catch {
62
+ throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
63
+ }
64
+ }
65
+ const resolved = await assertSandboxPath({
66
+ filePath: candidate,
67
+ cwd: params.sandboxRoot,
68
+ root: params.sandboxRoot,
69
+ });
70
+ return resolved.resolved;
71
+ }
41
72
  async function assertNoSymlink(relative, root) {
42
73
  if (!relative)
43
74
  return;
@@ -1,16 +1,76 @@
1
- import { makeMissingToolResult } from "./session-transcript-repair.js";
2
1
  import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
2
+ import { HARD_MAX_TOOL_RESULT_CHARS } from "./pi-embedded-runner/tool-result-truncation.js";
3
+ import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js";
4
+ const GUARD_TRUNCATION_SUFFIX = "\n\n⚠️ [Content truncated during persistence — original exceeded size limit. " +
5
+ "Use offset/limit parameters or request specific sections for large content.]";
6
+ /**
7
+ * Truncate oversized text content blocks in a tool result message.
8
+ * Returns the original message if under the limit, or a new message with
9
+ * truncated text blocks otherwise.
10
+ */
11
+ function capToolResultSize(msg) {
12
+ const role = msg.role;
13
+ if (role !== "toolResult") {
14
+ return msg;
15
+ }
16
+ const content = msg.content;
17
+ if (!Array.isArray(content)) {
18
+ return msg;
19
+ }
20
+ // Calculate total text size
21
+ let totalTextChars = 0;
22
+ for (const block of content) {
23
+ if (block && typeof block === "object" && block.type === "text") {
24
+ const text = block.text;
25
+ if (typeof text === "string") {
26
+ totalTextChars += text.length;
27
+ }
28
+ }
29
+ }
30
+ if (totalTextChars <= HARD_MAX_TOOL_RESULT_CHARS) {
31
+ return msg;
32
+ }
33
+ // Truncate proportionally
34
+ const newContent = content.map((block) => {
35
+ if (!block || typeof block !== "object" || block.type !== "text") {
36
+ return block;
37
+ }
38
+ const textBlock = block;
39
+ if (typeof textBlock.text !== "string") {
40
+ return block;
41
+ }
42
+ const blockShare = textBlock.text.length / totalTextChars;
43
+ const blockBudget = Math.max(2_000, Math.floor(HARD_MAX_TOOL_RESULT_CHARS * blockShare) - GUARD_TRUNCATION_SUFFIX.length);
44
+ if (textBlock.text.length <= blockBudget) {
45
+ return block;
46
+ }
47
+ // Try to cut at a newline boundary
48
+ let cutPoint = blockBudget;
49
+ const lastNewline = textBlock.text.lastIndexOf("\n", blockBudget);
50
+ if (lastNewline > blockBudget * 0.8) {
51
+ cutPoint = lastNewline;
52
+ }
53
+ return {
54
+ ...textBlock,
55
+ text: textBlock.text.slice(0, cutPoint) + GUARD_TRUNCATION_SUFFIX,
56
+ };
57
+ });
58
+ return { ...msg, content: newContent };
59
+ }
3
60
  function extractAssistantToolCalls(msg) {
4
61
  const content = msg.content;
5
- if (!Array.isArray(content))
62
+ if (!Array.isArray(content)) {
6
63
  return [];
64
+ }
7
65
  const toolCalls = [];
8
66
  for (const block of content) {
9
- if (!block || typeof block !== "object")
67
+ if (!block || typeof block !== "object") {
10
68
  continue;
69
+ }
11
70
  const rec = block;
12
- if (typeof rec.id !== "string" || !rec.id)
71
+ if (typeof rec.id !== "string" || !rec.id) {
13
72
  continue;
73
+ }
14
74
  if (rec.type === "toolCall" || rec.type === "toolUse" || rec.type === "functionCall") {
15
75
  toolCalls.push({
16
76
  id: rec.id,
@@ -22,11 +82,13 @@ function extractAssistantToolCalls(msg) {
22
82
  }
23
83
  function extractToolResultId(msg) {
24
84
  const toolCallId = msg.toolCallId;
25
- if (typeof toolCallId === "string" && toolCallId)
85
+ if (typeof toolCallId === "string" && toolCallId) {
26
86
  return toolCallId;
87
+ }
27
88
  const toolUseId = msg.toolUseId;
28
- if (typeof toolUseId === "string" && toolUseId)
89
+ if (typeof toolUseId === "string" && toolUseId) {
29
90
  return toolUseId;
91
+ }
30
92
  return null;
31
93
  }
32
94
  export function installSessionToolResultGuard(sessionManager, opts) {
@@ -38,8 +100,9 @@ export function installSessionToolResultGuard(sessionManager, opts) {
38
100
  };
39
101
  const allowSyntheticToolResults = opts?.allowSyntheticToolResults ?? true;
40
102
  const flushPendingToolResults = () => {
41
- if (pending.size === 0)
103
+ if (pending.size === 0) {
42
104
  return;
105
+ }
43
106
  if (allowSyntheticToolResults) {
44
107
  for (const [id, name] of pending.entries()) {
45
108
  const synthetic = makeMissingToolResult({ toolCallId: id, toolName: name });
@@ -53,24 +116,40 @@ export function installSessionToolResultGuard(sessionManager, opts) {
53
116
  pending.clear();
54
117
  };
55
118
  const guardedAppend = (message) => {
119
+ let nextMessage = message;
56
120
  const role = message.role;
57
- if (role === "toolResult") {
58
- const id = extractToolResultId(message);
121
+ if (role === "assistant") {
122
+ const sanitized = sanitizeToolCallInputs([message]);
123
+ if (sanitized.length === 0) {
124
+ if (allowSyntheticToolResults && pending.size > 0) {
125
+ flushPendingToolResults();
126
+ }
127
+ return undefined;
128
+ }
129
+ nextMessage = sanitized[0];
130
+ }
131
+ const nextRole = nextMessage.role;
132
+ if (nextRole === "toolResult") {
133
+ const id = extractToolResultId(nextMessage);
59
134
  const toolName = id ? pending.get(id) : undefined;
60
- if (id)
135
+ if (id) {
61
136
  pending.delete(id);
62
- return originalAppend(persistToolResult(message, {
137
+ }
138
+ // Apply hard size cap before persistence to prevent oversized tool results
139
+ // from consuming the entire context window on subsequent LLM calls.
140
+ const capped = capToolResultSize(nextMessage);
141
+ return originalAppend(persistToolResult(capped, {
63
142
  toolCallId: id ?? undefined,
64
143
  toolName,
65
144
  isSynthetic: false,
66
145
  }));
67
146
  }
68
- const toolCalls = role === "assistant"
69
- ? extractAssistantToolCalls(message)
147
+ const toolCalls = nextRole === "assistant"
148
+ ? extractAssistantToolCalls(nextMessage)
70
149
  : [];
71
150
  if (allowSyntheticToolResults) {
72
151
  // If previous tool calls are still pending, flush before non-tool results.
73
- if (pending.size > 0 && (toolCalls.length === 0 || role !== "assistant")) {
152
+ if (pending.size > 0 && (toolCalls.length === 0 || nextRole !== "assistant")) {
74
153
  flushPendingToolResults();
75
154
  }
76
155
  // If new tool calls arrive while older ones are pending, flush the old ones first.
@@ -78,7 +157,7 @@ export function installSessionToolResultGuard(sessionManager, opts) {
78
157
  flushPendingToolResults();
79
158
  }
80
159
  }
81
- const result = originalAppend(message);
160
+ const result = originalAppend(nextMessage);
82
161
  const sessionFile = sessionManager.getSessionFile?.();
83
162
  if (sessionFile) {
84
163
  emitSessionTranscriptUpdate(sessionFile);