@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
@@ -1,11 +1,16 @@
1
+ import JSON5 from "json5";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import JSON5 from "json5";
5
- import { getFileMtimeMs, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js";
5
+ import { parseByteSize } from "../../cli/parse-bytes.js";
6
+ import { parseDurationMs } from "../../cli/parse-duration.js";
7
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
6
8
  import { deliveryContextFromSession, mergeDeliveryContext, normalizeDeliveryContext, normalizeSessionDeliveryFields, } from "../../utils/delivery-context.js";
9
+ import { getFileMtimeMs, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js";
10
+ import { loadConfig } from "../config.js";
7
11
  import { deriveSessionMetaPatch } from "./metadata.js";
8
12
  import { mergeSessionEntry } from "./types.js";
13
+ const log = createSubsystemLogger("sessions/store");
9
14
  const SESSION_STORE_CACHE = new Map();
10
15
  const DEFAULT_SESSION_STORE_TTL_MS = 45_000; // 45 seconds (between 30-60s)
11
16
  function isSessionStoreRecord(value) {
@@ -29,7 +34,14 @@ function invalidateSessionStoreCache(storePath) {
29
34
  SESSION_STORE_CACHE.delete(storePath);
30
35
  }
31
36
  function normalizeSessionEntryDelivery(entry) {
32
- const normalized = normalizeSessionDeliveryFields(entry);
37
+ const normalized = normalizeSessionDeliveryFields({
38
+ channel: entry.channel,
39
+ lastChannel: entry.lastChannel,
40
+ lastTo: entry.lastTo,
41
+ lastAccountId: entry.lastAccountId,
42
+ lastThreadId: entry.lastThreadId ?? entry.deliveryContext?.threadId ?? entry.origin?.threadId,
43
+ deliveryContext: entry.deliveryContext,
44
+ });
33
45
  const nextDelivery = normalized.deliveryContext;
34
46
  const sameDelivery = (entry.deliveryContext?.channel ?? undefined) === nextDelivery?.channel &&
35
47
  (entry.deliveryContext?.to ?? undefined) === nextDelivery?.to &&
@@ -39,8 +51,9 @@ function normalizeSessionEntryDelivery(entry) {
39
51
  entry.lastTo === normalized.lastTo &&
40
52
  entry.lastAccountId === normalized.lastAccountId &&
41
53
  entry.lastThreadId === normalized.lastThreadId;
42
- if (sameDelivery && sameLast)
54
+ if (sameDelivery && sameLast) {
43
55
  return entry;
56
+ }
44
57
  return {
45
58
  ...entry,
46
59
  deliveryContext: nextDelivery,
@@ -50,10 +63,19 @@ function normalizeSessionEntryDelivery(entry) {
50
63
  lastThreadId: normalized.lastThreadId,
51
64
  };
52
65
  }
66
+ function removeThreadFromDeliveryContext(context) {
67
+ if (!context || context.threadId == null) {
68
+ return context;
69
+ }
70
+ const next = { ...context };
71
+ delete next.threadId;
72
+ return next;
73
+ }
53
74
  function normalizeSessionStore(store) {
54
75
  for (const [key, entry] of Object.entries(store)) {
55
- if (!entry)
76
+ if (!entry) {
56
77
  continue;
78
+ }
57
79
  const normalized = normalizeSessionEntryDelivery(entry);
58
80
  if (normalized !== entry) {
59
81
  store[key] = normalized;
@@ -92,8 +114,9 @@ export function loadSessionStore(storePath, opts = {}) {
92
114
  }
93
115
  // Best-effort migration: message provider → channel naming.
94
116
  for (const entry of Object.values(store)) {
95
- if (!entry || typeof entry !== "object")
117
+ if (!entry || typeof entry !== "object") {
96
118
  continue;
119
+ }
97
120
  const rec = entry;
98
121
  if (typeof rec.channel !== "string" && typeof rec.provider === "string") {
99
122
  rec.channel = rec.provider;
@@ -132,10 +155,233 @@ export function readSessionUpdatedAt(params) {
132
155
  return undefined;
133
156
  }
134
157
  }
135
- async function saveSessionStoreUnlocked(storePath, store) {
158
+ // ============================================================================
159
+ // Session Store Pruning, Capping & File Rotation
160
+ // ============================================================================
161
+ const DEFAULT_SESSION_PRUNE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
162
+ const DEFAULT_SESSION_MAX_ENTRIES = 500;
163
+ const DEFAULT_SESSION_ROTATE_BYTES = 10_485_760; // 10 MB
164
+ const DEFAULT_SESSION_MAINTENANCE_MODE = "warn";
165
+ function resolvePruneAfterMs(maintenance) {
166
+ const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays;
167
+ if (raw === undefined || raw === null || raw === "") {
168
+ return DEFAULT_SESSION_PRUNE_AFTER_MS;
169
+ }
170
+ try {
171
+ return parseDurationMs(String(raw).trim(), { defaultUnit: "d" });
172
+ }
173
+ catch {
174
+ return DEFAULT_SESSION_PRUNE_AFTER_MS;
175
+ }
176
+ }
177
+ function resolveRotateBytes(maintenance) {
178
+ const raw = maintenance?.rotateBytes;
179
+ if (raw === undefined || raw === null || raw === "") {
180
+ return DEFAULT_SESSION_ROTATE_BYTES;
181
+ }
182
+ try {
183
+ return parseByteSize(String(raw).trim(), { defaultUnit: "b" });
184
+ }
185
+ catch {
186
+ return DEFAULT_SESSION_ROTATE_BYTES;
187
+ }
188
+ }
189
+ /**
190
+ * Resolve maintenance settings from poolbot.json (`session.maintenance`).
191
+ * Falls back to built-in defaults when config is missing or unset.
192
+ */
193
+ export function resolveMaintenanceConfig() {
194
+ let maintenance;
195
+ try {
196
+ maintenance = loadConfig().session?.maintenance;
197
+ }
198
+ catch {
199
+ // Config may not be available (e.g. in tests). Use defaults.
200
+ }
201
+ return {
202
+ mode: maintenance?.mode ?? DEFAULT_SESSION_MAINTENANCE_MODE,
203
+ pruneAfterMs: resolvePruneAfterMs(maintenance),
204
+ maxEntries: maintenance?.maxEntries ?? DEFAULT_SESSION_MAX_ENTRIES,
205
+ rotateBytes: resolveRotateBytes(maintenance),
206
+ };
207
+ }
208
+ /**
209
+ * Remove entries whose `updatedAt` is older than the configured threshold.
210
+ * Entries without `updatedAt` are kept (cannot determine staleness).
211
+ * Mutates `store` in-place.
212
+ */
213
+ export function pruneStaleEntries(store, overrideMaxAgeMs, opts = {}) {
214
+ const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs;
215
+ const cutoffMs = Date.now() - maxAgeMs;
216
+ let pruned = 0;
217
+ for (const [key, entry] of Object.entries(store)) {
218
+ if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) {
219
+ delete store[key];
220
+ pruned++;
221
+ }
222
+ }
223
+ if (pruned > 0 && opts.log !== false) {
224
+ log.info("pruned stale session entries", { pruned, maxAgeMs });
225
+ }
226
+ return pruned;
227
+ }
228
+ /**
229
+ * Cap the store to the N most recently updated entries.
230
+ * Entries without `updatedAt` are sorted last (removed first when over limit).
231
+ * Mutates `store` in-place.
232
+ */
233
+ function getEntryUpdatedAt(entry) {
234
+ return entry?.updatedAt ?? Number.NEGATIVE_INFINITY;
235
+ }
236
+ export function getActiveSessionMaintenanceWarning(params) {
237
+ const activeSessionKey = params.activeSessionKey.trim();
238
+ if (!activeSessionKey) {
239
+ return null;
240
+ }
241
+ const activeEntry = params.store[activeSessionKey];
242
+ if (!activeEntry) {
243
+ return null;
244
+ }
245
+ const now = params.nowMs ?? Date.now();
246
+ const cutoffMs = now - params.pruneAfterMs;
247
+ const wouldPrune = activeEntry.updatedAt != null ? activeEntry.updatedAt < cutoffMs : false;
248
+ const keys = Object.keys(params.store);
249
+ const wouldCap = keys.length > params.maxEntries &&
250
+ keys
251
+ .toSorted((a, b) => getEntryUpdatedAt(params.store[b]) - getEntryUpdatedAt(params.store[a]))
252
+ .slice(params.maxEntries)
253
+ .includes(activeSessionKey);
254
+ if (!wouldPrune && !wouldCap) {
255
+ return null;
256
+ }
257
+ return {
258
+ activeSessionKey,
259
+ activeUpdatedAt: activeEntry.updatedAt,
260
+ totalEntries: keys.length,
261
+ pruneAfterMs: params.pruneAfterMs,
262
+ maxEntries: params.maxEntries,
263
+ wouldPrune,
264
+ wouldCap,
265
+ };
266
+ }
267
+ export function capEntryCount(store, overrideMax, opts = {}) {
268
+ const maxEntries = overrideMax ?? resolveMaintenanceConfig().maxEntries;
269
+ const keys = Object.keys(store);
270
+ if (keys.length <= maxEntries) {
271
+ return 0;
272
+ }
273
+ // Sort by updatedAt descending; entries without updatedAt go to the end (removed first).
274
+ const sorted = keys.toSorted((a, b) => {
275
+ const aTime = getEntryUpdatedAt(store[a]);
276
+ const bTime = getEntryUpdatedAt(store[b]);
277
+ return bTime - aTime;
278
+ });
279
+ const toRemove = sorted.slice(maxEntries);
280
+ for (const key of toRemove) {
281
+ delete store[key];
282
+ }
283
+ if (opts.log !== false) {
284
+ log.info("capped session entry count", { removed: toRemove.length, maxEntries });
285
+ }
286
+ return toRemove.length;
287
+ }
288
+ async function getSessionFileSize(storePath) {
289
+ try {
290
+ const stat = await fs.promises.stat(storePath);
291
+ return stat.size;
292
+ }
293
+ catch {
294
+ return null;
295
+ }
296
+ }
297
+ /**
298
+ * Rotate the sessions file if it exceeds the configured size threshold.
299
+ * Renames the current file to `sessions.json.bak.{timestamp}` and cleans up
300
+ * old rotation backups, keeping only the 3 most recent `.bak.*` files.
301
+ */
302
+ export async function rotateSessionFile(storePath, overrideBytes) {
303
+ const maxBytes = overrideBytes ?? resolveMaintenanceConfig().rotateBytes;
304
+ // Check current file size (file may not exist yet).
305
+ const fileSize = await getSessionFileSize(storePath);
306
+ if (fileSize == null) {
307
+ return false;
308
+ }
309
+ if (fileSize <= maxBytes) {
310
+ return false;
311
+ }
312
+ // Rotate: rename current file to .bak.{timestamp}
313
+ const backupPath = `${storePath}.bak.${Date.now()}`;
314
+ try {
315
+ await fs.promises.rename(storePath, backupPath);
316
+ log.info("rotated session store file", {
317
+ backupPath: path.basename(backupPath),
318
+ sizeBytes: fileSize,
319
+ });
320
+ }
321
+ catch {
322
+ // If rename fails (e.g. file disappeared), skip rotation.
323
+ return false;
324
+ }
325
+ // Clean up old backups — keep only the 3 most recent .bak.* files.
326
+ try {
327
+ const dir = path.dirname(storePath);
328
+ const baseName = path.basename(storePath);
329
+ const files = await fs.promises.readdir(dir);
330
+ const backups = files
331
+ .filter((f) => f.startsWith(`${baseName}.bak.`))
332
+ .toSorted()
333
+ .toReversed();
334
+ const maxBackups = 3;
335
+ if (backups.length > maxBackups) {
336
+ const toDelete = backups.slice(maxBackups);
337
+ for (const old of toDelete) {
338
+ await fs.promises.unlink(path.join(dir, old)).catch(() => undefined);
339
+ }
340
+ log.info("cleaned up old session store backups", { deleted: toDelete.length });
341
+ }
342
+ }
343
+ catch {
344
+ // Best-effort cleanup; don't fail the write.
345
+ }
346
+ return true;
347
+ }
348
+ async function saveSessionStoreUnlocked(storePath, store, opts) {
136
349
  // Invalidate cache on write to ensure consistency
137
350
  invalidateSessionStoreCache(storePath);
138
351
  normalizeSessionStore(store);
352
+ if (!opts?.skipMaintenance) {
353
+ // Resolve maintenance config once (avoids repeated loadConfig() calls).
354
+ const maintenance = resolveMaintenanceConfig();
355
+ const shouldWarnOnly = maintenance.mode === "warn";
356
+ if (shouldWarnOnly) {
357
+ const activeSessionKey = opts?.activeSessionKey?.trim();
358
+ if (activeSessionKey) {
359
+ const warning = getActiveSessionMaintenanceWarning({
360
+ store,
361
+ activeSessionKey,
362
+ pruneAfterMs: maintenance.pruneAfterMs,
363
+ maxEntries: maintenance.maxEntries,
364
+ });
365
+ if (warning) {
366
+ log.warn("session maintenance would evict active session; skipping enforcement", {
367
+ activeSessionKey: warning.activeSessionKey,
368
+ wouldPrune: warning.wouldPrune,
369
+ wouldCap: warning.wouldCap,
370
+ pruneAfterMs: warning.pruneAfterMs,
371
+ maxEntries: warning.maxEntries,
372
+ });
373
+ await opts?.onWarn?.(warning);
374
+ }
375
+ }
376
+ }
377
+ else {
378
+ // Prune stale entries and cap total count before serializing.
379
+ pruneStaleEntries(store, maintenance.pruneAfterMs);
380
+ capEntryCount(store, maintenance.maxEntries);
381
+ // Rotate the on-disk file if it exceeds the size threshold.
382
+ await rotateSessionFile(storePath, maintenance.rotateBytes);
383
+ }
384
+ }
139
385
  await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
140
386
  const json = JSON.stringify(store, null, 2);
141
387
  // Windows: avoid atomic rename swaps (can be flaky under concurrent access).
@@ -148,8 +394,9 @@ async function saveSessionStoreUnlocked(storePath, store) {
148
394
  const code = err && typeof err === "object" && "code" in err
149
395
  ? String(err.code)
150
396
  : null;
151
- if (code === "ENOENT")
397
+ if (code === "ENOENT") {
152
398
  return;
399
+ }
153
400
  throw err;
154
401
  }
155
402
  return;
@@ -177,8 +424,9 @@ async function saveSessionStoreUnlocked(storePath, store) {
177
424
  const code2 = err2 && typeof err2 === "object" && "code" in err2
178
425
  ? String(err2.code)
179
426
  : null;
180
- if (code2 === "ENOENT")
427
+ if (code2 === "ENOENT") {
181
428
  return;
429
+ }
182
430
  throw err2;
183
431
  }
184
432
  return;
@@ -189,17 +437,17 @@ async function saveSessionStoreUnlocked(storePath, store) {
189
437
  await fs.promises.rm(tmp, { force: true });
190
438
  }
191
439
  }
192
- export async function saveSessionStore(storePath, store) {
440
+ export async function saveSessionStore(storePath, store, opts) {
193
441
  await withSessionStoreLock(storePath, async () => {
194
- await saveSessionStoreUnlocked(storePath, store);
442
+ await saveSessionStoreUnlocked(storePath, store, opts);
195
443
  });
196
444
  }
197
- export async function updateSessionStore(storePath, mutator) {
445
+ export async function updateSessionStore(storePath, mutator, opts) {
198
446
  return await withSessionStoreLock(storePath, async () => {
199
447
  // Always re-read inside the lock to avoid clobbering concurrent writers.
200
448
  const store = loadSessionStore(storePath, { skipCache: true });
201
449
  const result = await mutator(store);
202
- await saveSessionStoreUnlocked(storePath, store);
450
+ await saveSessionStoreUnlocked(storePath, store, opts);
203
451
  return result;
204
452
  });
205
453
  }
@@ -235,11 +483,12 @@ async function withSessionStoreLock(storePath, fn, opts = {}) {
235
483
  await new Promise((r) => setTimeout(r, pollIntervalMs));
236
484
  continue;
237
485
  }
238
- if (code !== "EEXIST")
486
+ if (code !== "EEXIST") {
239
487
  throw err;
488
+ }
240
489
  const now = Date.now();
241
490
  if (now - startedAt > timeoutMs) {
242
- throw new Error(`timeout acquiring session store lock: ${lockPath}`);
491
+ throw new Error(`timeout acquiring session store lock: ${lockPath}`, { cause: err });
243
492
  }
244
493
  // Best-effort stale lock eviction (e.g. crashed process).
245
494
  try {
@@ -268,14 +517,16 @@ export async function updateSessionStoreEntry(params) {
268
517
  return await withSessionStoreLock(storePath, async () => {
269
518
  const store = loadSessionStore(storePath);
270
519
  const existing = store[sessionKey];
271
- if (!existing)
520
+ if (!existing) {
272
521
  return null;
522
+ }
273
523
  const patch = await update(existing);
274
- if (!patch)
524
+ if (!patch) {
275
525
  return existing;
526
+ }
276
527
  const next = mergeSessionEntry(existing, patch);
277
528
  store[sessionKey] = next;
278
- await saveSessionStoreUnlocked(storePath, store);
529
+ await saveSessionStoreUnlocked(storePath, store, { activeSessionKey: sessionKey });
279
530
  return next;
280
531
  });
281
532
  }
@@ -290,14 +541,16 @@ export async function recordSessionMetaFromInbound(params) {
290
541
  existing,
291
542
  groupResolution: params.groupResolution,
292
543
  });
293
- if (!patch)
544
+ if (!patch) {
294
545
  return existing ?? null;
295
- if (!existing && !createIfMissing)
546
+ }
547
+ if (!existing && !createIfMissing) {
296
548
  return null;
549
+ }
297
550
  const next = mergeSessionEntry(existing, patch);
298
551
  store[sessionKey] = next;
299
552
  return next;
300
- });
553
+ }, { activeSessionKey: sessionKey });
301
554
  }
302
555
  export async function updateLastRoute(params) {
303
556
  const { storePath, sessionKey, channel, to, accountId, threadId, ctx } = params;
@@ -313,7 +566,22 @@ export async function updateLastRoute(params) {
313
566
  threadId,
314
567
  });
315
568
  const mergedInput = mergeDeliveryContext(explicitContext, inlineContext);
316
- const merged = mergeDeliveryContext(mergedInput, deliveryContextFromSession(existing));
569
+ const explicitDeliveryContext = params.deliveryContext;
570
+ const explicitThreadFromDeliveryContext = explicitDeliveryContext != null &&
571
+ Object.prototype.hasOwnProperty.call(explicitDeliveryContext, "threadId")
572
+ ? explicitDeliveryContext.threadId
573
+ : undefined;
574
+ const explicitThreadValue = explicitThreadFromDeliveryContext ??
575
+ (threadId != null && threadId !== "" ? threadId : undefined);
576
+ const explicitRouteProvided = Boolean(explicitContext?.channel ||
577
+ explicitContext?.to ||
578
+ inlineContext?.channel ||
579
+ inlineContext?.to);
580
+ const clearThreadFromFallback = explicitRouteProvided && explicitThreadValue == null;
581
+ const fallbackContext = clearThreadFromFallback
582
+ ? removeThreadFromDeliveryContext(deliveryContextFromSession(existing))
583
+ : deliveryContextFromSession(existing);
584
+ const merged = mergeDeliveryContext(mergedInput, fallbackContext);
317
585
  const normalized = normalizeSessionDeliveryFields({
318
586
  deliveryContext: {
319
587
  channel: merged?.channel,
@@ -340,7 +608,7 @@ export async function updateLastRoute(params) {
340
608
  };
341
609
  const next = mergeSessionEntry(existing, metaPatch ? { ...basePatch, ...metaPatch } : basePatch);
342
610
  store[sessionKey] = next;
343
- await saveSessionStoreUnlocked(storePath, store);
611
+ await saveSessionStoreUnlocked(storePath, store, { activeSessionKey: sessionKey });
344
612
  return next;
345
613
  });
346
614
  }
@@ -23,6 +23,8 @@ export const AgentDefaultsSchema = z
23
23
  alias: z.string().optional(),
24
24
  /** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
25
25
  params: z.record(z.string(), z.unknown()).optional(),
26
+ /** Enable streaming for this model (default: true, false for Ollama to avoid SDK issue #1205). */
27
+ streaming: z.boolean().optional(),
26
28
  })
27
29
  .strict())
28
30
  .optional(),
@@ -135,6 +137,7 @@ export const AgentDefaultsSchema = z
135
137
  .strict(),
136
138
  ])
137
139
  .optional(),
140
+ thinking: z.string().optional(),
138
141
  })
139
142
  .strict()
140
143
  .optional(),
@@ -17,6 +17,7 @@ export const HeartbeatSchema = z
17
17
  includeReasoning: z.boolean().optional(),
18
18
  target: z.string().optional(),
19
19
  to: z.string().optional(),
20
+ accountId: z.string().optional(),
20
21
  prompt: z.string().optional(),
21
22
  ackMaxChars: z.number().int().nonnegative().optional(),
22
23
  })
@@ -278,13 +279,16 @@ export const MemorySearchSchema = z
278
279
  .object({
279
280
  enabled: z.boolean().optional(),
280
281
  sources: z.array(z.union([z.literal("memory"), z.literal("sessions")])).optional(),
282
+ extraPaths: z.array(z.string()).optional(),
281
283
  experimental: z
282
284
  .object({
283
285
  sessionMemory: z.boolean().optional(),
284
286
  })
285
287
  .strict()
286
288
  .optional(),
287
- provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
289
+ provider: z
290
+ .union([z.literal("openai"), z.literal("local"), z.literal("gemini"), z.literal("voyage")])
291
+ .optional(),
288
292
  remote: z
289
293
  .object({
290
294
  baseUrl: z.string().optional(),
@@ -304,7 +308,13 @@ export const MemorySearchSchema = z
304
308
  .strict()
305
309
  .optional(),
306
310
  fallback: z
307
- .union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
311
+ .union([
312
+ z.literal("openai"),
313
+ z.literal("gemini"),
314
+ z.literal("local"),
315
+ z.literal("voyage"),
316
+ z.literal("none"),
317
+ ])
308
318
  .optional(),
309
319
  model: z.string().optional(),
310
320
  local: z
@@ -414,6 +424,7 @@ export const AgentEntrySchema = z
414
424
  .strict(),
415
425
  ])
416
426
  .optional(),
427
+ thinking: z.string().optional(),
417
428
  })
418
429
  .strict()
419
430
  .optional(),