@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
@@ -6,7 +6,7 @@ import { getCliSessionId } from "../../agents/cli-session.js";
6
6
  import { runWithModelFallback } from "../../agents/model-fallback.js";
7
7
  import { isCliProvider } from "../../agents/model-selection.js";
8
8
  import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
9
- import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
9
+ import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, isTransientHttpError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
10
10
  import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, resolveSessionTranscriptPath, updateSessionStore, } from "../../config/sessions.js";
11
11
  import { logVerbose } from "../../globals.js";
12
12
  import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
@@ -19,8 +19,10 @@ import { createBlockReplyPayloadKey } from "./block-reply-pipeline.js";
19
19
  import { parseReplyDirectives } from "./reply-directives.js";
20
20
  import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
21
21
  export async function runAgentTurnWithFallback(params) {
22
+ const TRANSIENT_HTTP_RETRY_DELAY_MS = 2_500;
22
23
  let didLogHeartbeatStrip = false;
23
24
  let autoCompactionCompleted = false;
25
+ let didRetryTransientHttpError = false;
24
26
  // Track payloads sent directly (not via pipeline) during tool flush to avoid duplicates.
25
27
  const directlySentBlockKeys = new Set();
26
28
  const runId = params.opts?.runId ?? crypto.randomUUID();
@@ -36,6 +38,7 @@ export async function runAgentTurnWithFallback(params) {
36
38
  let fallbackProvider = params.followupRun.run.provider;
37
39
  let fallbackModel = params.followupRun.run.model;
38
40
  let didResetAfterCompactionFailure = false;
41
+ let fallbackAttempts = [];
39
42
  while (true) {
40
43
  try {
41
44
  const allowPartialStream = !(params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream);
@@ -59,9 +62,13 @@ export async function runAgentTurnWithFallback(params) {
59
62
  if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
60
63
  return { skip: true };
61
64
  }
62
- if (!text)
65
+ if (!text) {
66
+ // Allow media-only payloads through (no text but has media)
67
+ if (payload.mediaUrls?.length)
68
+ return { text: undefined, skip: false };
63
69
  return { skip: true };
64
- const sanitized = sanitizeUserFacingText(text);
70
+ }
71
+ const sanitized = sanitizeUserFacingText(text, { errorContext: Boolean(payload.isError) });
65
72
  if (!sanitized.trim())
66
73
  return { skip: true };
67
74
  return { text: sanitized, skip: false };
@@ -112,6 +119,7 @@ export async function runAgentTurnWithFallback(params) {
112
119
  thinkLevel: params.followupRun.run.thinkLevel,
113
120
  timeoutMs: params.followupRun.run.timeoutMs,
114
121
  runId,
122
+ agentId: resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
115
123
  extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
116
124
  ownerNumbers: params.followupRun.run.ownerNumbers,
117
125
  cliSessionId,
@@ -348,6 +356,16 @@ export async function runAgentTurnWithFallback(params) {
348
356
  runResult = fallbackResult.result;
349
357
  fallbackProvider = fallbackResult.provider;
350
358
  fallbackModel = fallbackResult.model;
359
+ fallbackAttempts = Array.isArray(fallbackResult.attempts)
360
+ ? fallbackResult.attempts.map((attempt) => ({
361
+ provider: String(attempt.provider ?? ""),
362
+ model: String(attempt.model ?? ""),
363
+ error: String(attempt.error ?? ""),
364
+ reason: attempt.reason ? String(attempt.reason) : undefined,
365
+ status: typeof attempt.status === "number" ? attempt.status : undefined,
366
+ code: attempt.code ? String(attempt.code) : undefined,
367
+ }))
368
+ : [];
351
369
  // Some embedded runs surface context overflow as an error payload instead of throwing.
352
370
  // Treat those as a session-level failure and auto-recover by starting a fresh session.
353
371
  const embeddedError = runResult.meta?.error;
@@ -382,6 +400,7 @@ export async function runAgentTurnWithFallback(params) {
382
400
  const isCompactionFailure = isCompactionFailureError(message);
383
401
  const isSessionCorruption = /function call turn comes immediately after/i.test(message);
384
402
  const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
403
+ const isTransientHttp = isTransientHttpError(message);
385
404
  if (isCompactionFailure &&
386
405
  !didResetAfterCompactionFailure &&
387
406
  (await params.resetSessionAfterCompactionFailure(message))) {
@@ -404,6 +423,14 @@ export async function runAgentTurnWithFallback(params) {
404
423
  };
405
424
  }
406
425
  }
426
+ if (isTransientHttp && !didRetryTransientHttpError) {
427
+ didRetryTransientHttpError = true;
428
+ defaultRuntime.error(`Transient HTTP provider error before reply (${message}). Retrying once in ${TRANSIENT_HTTP_RETRY_DELAY_MS}ms.`);
429
+ await new Promise((resolve) => {
430
+ setTimeout(resolve, TRANSIENT_HTTP_RETRY_DELAY_MS);
431
+ });
432
+ continue;
433
+ }
407
434
  // Auto-recover from Gemini session corruption by resetting the session
408
435
  if (isSessionCorruption &&
409
436
  params.sessionKey &&
@@ -441,7 +468,10 @@ export async function runAgentTurnWithFallback(params) {
441
468
  };
442
469
  }
443
470
  defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
444
- const trimmedMessage = message.replace(/\.\s*$/, "");
471
+ const safeMessage = isTransientHttp
472
+ ? sanitizeUserFacingText(message, { errorContext: true })
473
+ : message;
474
+ const trimmedMessage = safeMessage.replace(/\.\s*$/, "");
445
475
  const fallbackText = isContextOverflow
446
476
  ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
447
477
  : isRoleOrderingError
@@ -460,6 +490,8 @@ export async function runAgentTurnWithFallback(params) {
460
490
  runResult,
461
491
  fallbackProvider,
462
492
  fallbackModel,
493
+ runId,
494
+ fallbackAttempts,
463
495
  didLogHeartbeatStrip,
464
496
  autoCompactionCompleted,
465
497
  directlySentBlockKeys: directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
@@ -7,8 +7,11 @@ import { isCliProvider } from "../../agents/model-selection.js";
7
7
  import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
8
8
  import { hasNonzeroUsage } from "../../agents/usage.js";
9
9
  import { resolveAgentIdFromSessionKey, resolveSessionFilePath, resolveSessionTranscriptPath, updateSessionStore, updateSessionStoreEntry, } from "../../config/sessions.js";
10
+ import { emitAgentEvent } from "../../infra/agent-events.js";
11
+ import { emitDiagnosticEvent, isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
10
12
  import { defaultRuntime } from "../../runtime.js";
11
13
  import { estimateUsageCost, resolveModelCostConfig } from "../../utils/usage-format.js";
14
+ import { buildFallbackClearedNotice, buildFallbackNotice, resolveFallbackTransition, } from "../fallback-state.js";
12
15
  import { resolveResponseUsageMode } from "../thinking.js";
13
16
  import { runAgentTurnWithFallback } from "./agent-runner-execution.js";
14
17
  import { createShouldEmitToolOutput, createShouldEmitToolResult, finalizeWithFollowup, isAudioPayload, signalTypingIfNeeded, } from "./agent-runner-helpers.js";
@@ -20,11 +23,41 @@ import { resolveBlockStreamingCoalescing } from "./block-streaming.js";
20
23
  import { createFollowupRunner } from "./followup-runner.js";
21
24
  import { enqueueFollowupRun } from "./queue.js";
22
25
  import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
23
- import { persistSessionUsageUpdate } from "./session-usage.js";
24
- import { incrementCompactionCount } from "./session-updates.js";
26
+ import { incrementRunCompactionCount, persistRunSessionUsage } from "./session-run-accounting.js";
25
27
  import { createTypingSignaler } from "./typing-mode.js";
26
- import { emitDiagnosticEvent, isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
27
28
  const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
29
+ const UNSCHEDULED_REMINDER_NOTE = "Note: I did not schedule a reminder in this turn, so this will not trigger automatically.";
30
+ const REMINDER_COMMITMENT_PATTERNS = [
31
+ /\b(?:i\s*['']?ll|i will)\s+(?:make sure to\s+)?(?:remember|remind|ping|follow up|follow-up|check back|circle back)\b/i,
32
+ /\b(?:i\s*['']?ll|i will)\s+(?:set|create|schedule)\s+(?:a\s+)?reminder\b/i,
33
+ ];
34
+ function hasUnbackedReminderCommitment(text) {
35
+ const normalized = text.toLowerCase();
36
+ if (!normalized.trim()) {
37
+ return false;
38
+ }
39
+ if (normalized.includes(UNSCHEDULED_REMINDER_NOTE.toLowerCase())) {
40
+ return false;
41
+ }
42
+ return REMINDER_COMMITMENT_PATTERNS.some((pattern) => pattern.test(text));
43
+ }
44
+ function appendUnscheduledReminderNote(payloads) {
45
+ let appended = false;
46
+ return payloads.map((payload) => {
47
+ if (appended || payload.isError || typeof payload.text !== "string") {
48
+ return payload;
49
+ }
50
+ if (!hasUnbackedReminderCommitment(payload.text)) {
51
+ return payload;
52
+ }
53
+ appended = true;
54
+ const trimmed = payload.text.trimEnd();
55
+ return {
56
+ ...payload,
57
+ text: `${trimmed}\n\n${UNSCHEDULED_REMINDER_NOTE}`,
58
+ };
59
+ });
60
+ }
28
61
  export async function runReplyAgent(params) {
29
62
  const { commandBody, followupRun, queueKey, resolvedQueue, shouldSteer, shouldFollowup, isActive, isStreaming, opts, typing, sessionEntry, sessionStore, sessionKey, storePath, defaultModel, agentCfgContextTokens, resolvedVerboseLevel, isNewSession, blockStreamingEnabled, blockReplyChunking, resolvedBlockStreamingBreak, sessionCtx, shouldInjectGroupIntro, typingMode, } = params;
30
63
  let activeSessionEntry = sessionEntry;
@@ -64,27 +97,7 @@ export async function runReplyAgent(params) {
64
97
  buffer: createAudioAsVoiceBuffer({ isAudioPayload }),
65
98
  })
66
99
  : null;
67
- if (shouldSteer && isStreaming) {
68
- const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt);
69
- if (steered && !shouldFollowup) {
70
- if (activeSessionEntry && activeSessionStore && sessionKey) {
71
- const updatedAt = Date.now();
72
- activeSessionEntry.updatedAt = updatedAt;
73
- activeSessionStore[sessionKey] = activeSessionEntry;
74
- if (storePath) {
75
- await updateSessionStoreEntry({
76
- storePath,
77
- sessionKey,
78
- update: async () => ({ updatedAt }),
79
- });
80
- }
81
- }
82
- typing.cleanup();
83
- return undefined;
84
- }
85
- }
86
- if (isActive && (shouldFollowup || resolvedQueue.mode === "steer")) {
87
- enqueueFollowupRun(queueKey, followupRun, resolvedQueue);
100
+ const touchActiveSessionEntry = async () => {
88
101
  if (activeSessionEntry && activeSessionStore && sessionKey) {
89
102
  const updatedAt = Date.now();
90
103
  activeSessionEntry.updatedAt = updatedAt;
@@ -97,6 +110,18 @@ export async function runReplyAgent(params) {
97
110
  });
98
111
  }
99
112
  }
113
+ };
114
+ if (shouldSteer && isStreaming) {
115
+ const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt);
116
+ if (steered && !shouldFollowup) {
117
+ await touchActiveSessionEntry();
118
+ typing.cleanup();
119
+ return undefined;
120
+ }
121
+ }
122
+ if (isActive && (shouldFollowup || resolvedQueue.mode === "steer")) {
123
+ enqueueFollowupRun(queueKey, followupRun, resolvedQueue);
124
+ await touchActiveSessionEntry();
100
125
  typing.cleanup();
101
126
  return undefined;
102
127
  }
@@ -141,6 +166,9 @@ export async function runReplyAgent(params) {
141
166
  updatedAt: Date.now(),
142
167
  systemSent: false,
143
168
  abortedLastRun: false,
169
+ fallbackNoticeSelectedModel: undefined,
170
+ fallbackNoticeActiveModel: undefined,
171
+ fallbackNoticeReason: undefined,
144
172
  };
145
173
  const agentId = resolveAgentIdFromSessionKey(sessionKey);
146
174
  const nextSessionFile = resolveSessionTranscriptPath(nextSessionId, agentId, sessionCtx.MessageThreadId);
@@ -213,7 +241,7 @@ export async function runReplyAgent(params) {
213
241
  if (runOutcome.kind === "final") {
214
242
  return finalizeWithFollowup(runOutcome.payload, queueKey, runFollowupTurn);
215
243
  }
216
- const { runResult, fallbackProvider, fallbackModel, directlySentBlockKeys } = runOutcome;
244
+ const { runId, runResult, fallbackProvider, fallbackModel, fallbackAttempts, directlySentBlockKeys, } = runOutcome;
217
245
  let { didLogHeartbeatStrip, autoCompactionCompleted } = runOutcome;
218
246
  if (shouldInjectGroupIntro &&
219
247
  activeSessionEntry &&
@@ -243,24 +271,62 @@ export async function runReplyAgent(params) {
243
271
  if (pendingToolTasks.size > 0) {
244
272
  await Promise.allSettled(pendingToolTasks);
245
273
  }
246
- const usage = runResult.meta.agentMeta?.usage;
247
- const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
248
- const providerUsed = runResult.meta.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider;
274
+ const usage = runResult.meta?.agentMeta?.usage;
275
+ const promptTokens = runResult.meta?.agentMeta?.promptTokens;
276
+ const modelUsed = runResult.meta?.agentMeta?.model ?? fallbackModel ?? defaultModel;
277
+ const providerUsed = runResult.meta?.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider;
278
+ const verboseEnabled = resolvedVerboseLevel !== "off";
279
+ const selectedProvider = followupRun.run.provider;
280
+ const selectedModel = followupRun.run.model;
281
+ const fallbackStateEntry = activeSessionEntry ?? (sessionKey ? activeSessionStore?.[sessionKey] : undefined);
282
+ const fallbackTransition = resolveFallbackTransition({
283
+ selectedProvider,
284
+ selectedModel,
285
+ activeProvider: providerUsed,
286
+ activeModel: modelUsed,
287
+ attempts: fallbackAttempts,
288
+ state: fallbackStateEntry,
289
+ });
290
+ if (fallbackTransition.stateChanged) {
291
+ if (fallbackStateEntry) {
292
+ fallbackStateEntry.fallbackNoticeSelectedModel = fallbackTransition.nextState.selectedModel;
293
+ fallbackStateEntry.fallbackNoticeActiveModel = fallbackTransition.nextState.activeModel;
294
+ fallbackStateEntry.fallbackNoticeReason = fallbackTransition.nextState.reason;
295
+ fallbackStateEntry.updatedAt = Date.now();
296
+ activeSessionEntry = fallbackStateEntry;
297
+ }
298
+ if (sessionKey && fallbackStateEntry && activeSessionStore) {
299
+ activeSessionStore[sessionKey] = fallbackStateEntry;
300
+ }
301
+ if (sessionKey && storePath) {
302
+ await updateSessionStoreEntry({
303
+ storePath,
304
+ sessionKey,
305
+ update: async () => ({
306
+ fallbackNoticeSelectedModel: fallbackTransition.nextState.selectedModel,
307
+ fallbackNoticeActiveModel: fallbackTransition.nextState.activeModel,
308
+ fallbackNoticeReason: fallbackTransition.nextState.reason,
309
+ }),
310
+ });
311
+ }
312
+ }
249
313
  const cliSessionId = isCliProvider(providerUsed, cfg)
250
- ? runResult.meta.agentMeta?.sessionId?.trim()
314
+ ? runResult.meta?.agentMeta?.sessionId?.trim()
251
315
  : undefined;
252
316
  const contextTokensUsed = agentCfgContextTokens ??
253
317
  lookupContextTokens(modelUsed) ??
254
318
  activeSessionEntry?.contextTokens ??
255
319
  DEFAULT_CONTEXT_TOKENS;
256
- await persistSessionUsageUpdate({
320
+ await persistRunSessionUsage({
257
321
  storePath,
258
322
  sessionKey,
259
323
  usage,
324
+ lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
325
+ promptTokens,
260
326
  modelUsed,
261
327
  providerUsed,
262
328
  contextTokensUsed,
263
- systemPromptReport: runResult.meta.systemPromptReport,
329
+ systemPromptReport: runResult.meta?.systemPromptReport,
264
330
  cliSessionId,
265
331
  });
266
332
  // Drain any late tool/block deliveries before deciding there's "nothing to send".
@@ -288,7 +354,14 @@ export async function runReplyAgent(params) {
288
354
  didLogHeartbeatStrip = payloadResult.didLogHeartbeatStrip;
289
355
  if (replyPayloads.length === 0)
290
356
  return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
291
- await signalTypingIfNeeded(replyPayloads, typingSignals);
357
+ const successfulCronAdds = runResult.successfulCronAdds ?? 0;
358
+ const hasReminderCommitment = replyPayloads.some((payload) => !payload.isError &&
359
+ typeof payload.text === "string" &&
360
+ hasUnbackedReminderCommitment(payload.text));
361
+ const guardedReplyPayloads = hasReminderCommitment && successfulCronAdds === 0
362
+ ? appendUnscheduledReminderNote(replyPayloads)
363
+ : replyPayloads;
364
+ await signalTypingIfNeeded(guardedReplyPayloads, typingSignals);
292
365
  if (isDiagnosticsEnabled(cfg) && hasNonzeroUsage(usage)) {
293
366
  const input = usage.input ?? 0;
294
367
  const output = usage.output ?? 0;
@@ -323,6 +396,7 @@ export async function runReplyAgent(params) {
323
396
  },
324
397
  costUsd,
325
398
  durationMs: Date.now() - runStartedAt,
399
+ lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
326
400
  });
327
401
  }
328
402
  const responseUsageRaw = activeSessionEntry?.responseUsage ??
@@ -349,23 +423,75 @@ export async function runReplyAgent(params) {
349
423
  if (formatted)
350
424
  responseUsageLine = formatted;
351
425
  }
352
- // If verbose is enabled and this is a new session, prepend a session hint.
353
- let finalPayloads = replyPayloads;
354
- const verboseEnabled = resolvedVerboseLevel !== "off";
426
+ let finalPayloads = guardedReplyPayloads;
427
+ const verboseNotices = [];
428
+ if (activeIsNewSession && verboseEnabled) {
429
+ verboseNotices.push(`🧭 New session: ${followupRun.run.sessionId}`);
430
+ }
431
+ if (fallbackTransition.fallbackTransitioned) {
432
+ const notice = buildFallbackNotice({
433
+ selectedProvider,
434
+ selectedModel,
435
+ activeProvider: providerUsed,
436
+ activeModel: modelUsed,
437
+ attempts: fallbackAttempts,
438
+ });
439
+ if (runId) {
440
+ emitAgentEvent({
441
+ runId,
442
+ stream: "lifecycle",
443
+ data: {
444
+ kind: "model.fallback.transitioned",
445
+ selected: fallbackTransition.selectedModelRef,
446
+ active: fallbackTransition.activeModelRef,
447
+ reason: fallbackTransition.reasonSummary,
448
+ attempts: fallbackTransition.attemptSummaries,
449
+ },
450
+ sessionKey,
451
+ });
452
+ }
453
+ if (notice && verboseEnabled) {
454
+ verboseNotices.push(notice);
455
+ }
456
+ }
457
+ if (fallbackTransition.fallbackCleared) {
458
+ const notice = buildFallbackClearedNotice({
459
+ selectedProvider,
460
+ selectedModel,
461
+ previousActiveModel: fallbackTransition.previousState.activeModel,
462
+ });
463
+ if (runId) {
464
+ emitAgentEvent({
465
+ runId,
466
+ stream: "lifecycle",
467
+ data: {
468
+ kind: "model.fallback.cleared",
469
+ selected: fallbackTransition.selectedModelRef,
470
+ previousActive: fallbackTransition.previousState.activeModel,
471
+ },
472
+ sessionKey,
473
+ });
474
+ }
475
+ if (verboseEnabled) {
476
+ verboseNotices.push(notice);
477
+ }
478
+ }
355
479
  if (autoCompactionCompleted) {
356
- const count = await incrementCompactionCount({
480
+ const count = await incrementRunCompactionCount({
357
481
  sessionEntry: activeSessionEntry,
358
482
  sessionStore: activeSessionStore,
359
483
  sessionKey,
360
484
  storePath,
485
+ lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
486
+ contextTokensUsed,
361
487
  });
362
488
  if (verboseEnabled) {
363
489
  const suffix = typeof count === "number" ? ` (count ${count})` : "";
364
- finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads];
490
+ verboseNotices.push(`🧹 Auto-compaction complete${suffix}.`);
365
491
  }
366
492
  }
367
- if (verboseEnabled && activeIsNewSession) {
368
- finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads];
493
+ if (verboseNotices.length > 0) {
494
+ finalPayloads = [...verboseNotices.map((text) => ({ text })), ...finalPayloads];
369
495
  }
370
496
  if (responseUsageLine) {
371
497
  finalPayloads = appendUsageLine(finalPayloads, responseUsageLine);
@@ -0,0 +1,13 @@
1
+ import { parseSlashCommandWithSetUnset } from "./commands-setunset.js";
2
+ export function parseStandardSetUnsetSlashCommand(params) {
3
+ return parseSlashCommandWithSetUnset({
4
+ raw: params.raw,
5
+ slash: params.slash,
6
+ invalidMessage: params.invalidMessage,
7
+ usageMessage: params.usageMessage,
8
+ onKnownAction: params.onKnownAction,
9
+ onSet: params.onSet ?? ((path, value) => ({ action: "set", path, value })),
10
+ onUnset: params.onUnset ?? ((path) => ({ action: "unset", path })),
11
+ onError: params.onError ?? ((message) => ({ action: "error", message })),
12
+ });
13
+ }
@@ -16,6 +16,30 @@ function normalizeTimeoutMs(raw, fallback) {
16
16
  const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
17
17
  return value < 0 ? fallback : value;
18
18
  }
19
+ function normalizeStringList(raw) {
20
+ if (!Array.isArray(raw) || raw.length === 0) {
21
+ return undefined;
22
+ }
23
+ const values = raw
24
+ .map((value) => value.trim())
25
+ .filter((value) => value.length > 0);
26
+ return values.length > 0 ? values : undefined;
27
+ }
28
+ function resolveBrowserSsrFPolicy(cfg) {
29
+ const allowPrivateNetwork = cfg?.ssrfPolicy?.allowPrivateNetwork;
30
+ const allowedHostnames = normalizeStringList(cfg?.ssrfPolicy?.allowedHostnames);
31
+ const hostnameAllowlist = normalizeStringList(cfg?.ssrfPolicy?.hostnameAllowlist);
32
+ if (allowPrivateNetwork === undefined &&
33
+ allowedHostnames === undefined &&
34
+ hostnameAllowlist === undefined) {
35
+ return undefined;
36
+ }
37
+ return {
38
+ ...(allowPrivateNetwork === true ? { allowPrivateNetwork: true } : {}),
39
+ ...(allowedHostnames ? { allowedHostnames } : {}),
40
+ ...(hostnameAllowlist ? { hostnameAllowlist } : {}),
41
+ };
42
+ }
19
43
  export function parseHttpUrl(raw, label) {
20
44
  const trimmed = raw.trim();
21
45
  const parsed = new URL(trimmed);
@@ -113,6 +137,7 @@ export function resolveBrowserConfig(cfg, rootConfig) {
113
137
  (profiles[DEFAULT_BROWSER_DEFAULT_PROFILE_NAME]
114
138
  ? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME
115
139
  : DEFAULT_CLAWD_BROWSER_PROFILE_NAME);
140
+ const ssrfPolicy = resolveBrowserSsrFPolicy(cfg);
116
141
  return {
117
142
  enabled,
118
143
  evaluateEnabled,
@@ -129,6 +154,7 @@ export function resolveBrowserConfig(cfg, rootConfig) {
129
154
  attachOnly,
130
155
  defaultProfile,
131
156
  profiles,
157
+ ssrfPolicy,
132
158
  };
133
159
  }
134
160
  /**
@@ -0,0 +1,31 @@
1
+ import { resolvePinnedHostnameWithPolicy, } from "../infra/net/ssrf.js";
2
+ const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
3
+ export class InvalidBrowserNavigationUrlError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "InvalidBrowserNavigationUrlError";
7
+ }
8
+ }
9
+ export function withBrowserNavigationPolicy(ssrfPolicy) {
10
+ return ssrfPolicy ? { ssrfPolicy } : {};
11
+ }
12
+ export async function assertBrowserNavigationAllowed(opts) {
13
+ const rawUrl = String(opts.url ?? "").trim();
14
+ if (!rawUrl) {
15
+ throw new InvalidBrowserNavigationUrlError("url is required");
16
+ }
17
+ let parsed;
18
+ try {
19
+ parsed = new URL(rawUrl);
20
+ }
21
+ catch {
22
+ throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl}`);
23
+ }
24
+ if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
25
+ return;
26
+ }
27
+ await resolvePinnedHostnameWithPolicy(parsed.hostname, {
28
+ lookupFn: opts.lookupFn,
29
+ policy: opts.ssrfPolicy,
30
+ });
31
+ }