@poolzin/pool-bot 2026.2.23 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,15 +1,109 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { resolveGatewayLaunchAgentLabel, resolveGatewaySystemdServiceName, } from "../daemon/constants.js";
3
+ import { createSubsystemLogger } from "../logging/subsystem.js";
3
4
  const SPAWN_TIMEOUT_MS = 2000;
4
5
  const SIGUSR1_AUTH_GRACE_MS = 5000;
6
+ const DEFAULT_DEFERRAL_POLL_MS = 500;
7
+ const DEFAULT_DEFERRAL_MAX_WAIT_MS = 30_000;
8
+ const RESTART_COOLDOWN_MS = 30_000;
9
+ const restartLog = createSubsystemLogger("restart");
5
10
  let sigusr1AuthorizedCount = 0;
6
11
  let sigusr1AuthorizedUntil = 0;
7
12
  let sigusr1ExternalAllowed = false;
13
+ let preRestartCheck = null;
14
+ let restartCycleToken = 0;
15
+ let emittedRestartToken = 0;
16
+ let consumedRestartToken = 0;
17
+ let lastRestartEmittedAt = 0;
18
+ let pendingRestartTimer = null;
19
+ let pendingRestartDueAt = 0;
20
+ let pendingRestartReason;
21
+ function hasUnconsumedRestartSignal() {
22
+ return emittedRestartToken > consumedRestartToken;
23
+ }
24
+ function clearPendingScheduledRestart() {
25
+ if (pendingRestartTimer) {
26
+ clearTimeout(pendingRestartTimer);
27
+ }
28
+ pendingRestartTimer = null;
29
+ pendingRestartDueAt = 0;
30
+ pendingRestartReason = undefined;
31
+ }
32
+ function summarizeChangedPaths(paths, maxPaths = 6) {
33
+ if (!Array.isArray(paths) || paths.length === 0) {
34
+ return null;
35
+ }
36
+ if (paths.length <= maxPaths) {
37
+ return paths.join(",");
38
+ }
39
+ const head = paths.slice(0, maxPaths).join(",");
40
+ return `${head},+${paths.length - maxPaths} more`;
41
+ }
42
+ function formatRestartAudit(audit) {
43
+ const actor = typeof audit?.actor === "string" && audit.actor.trim() ? audit.actor.trim() : null;
44
+ const deviceId = typeof audit?.deviceId === "string" && audit.deviceId.trim() ? audit.deviceId.trim() : null;
45
+ const clientIp = typeof audit?.clientIp === "string" && audit.clientIp.trim() ? audit.clientIp.trim() : null;
46
+ const changed = summarizeChangedPaths(audit?.changedPaths);
47
+ const fields = [];
48
+ if (actor) {
49
+ fields.push(`actor=${actor}`);
50
+ }
51
+ if (deviceId) {
52
+ fields.push(`device=${deviceId}`);
53
+ }
54
+ if (clientIp) {
55
+ fields.push(`ip=${clientIp}`);
56
+ }
57
+ if (changed) {
58
+ fields.push(`changedPaths=${changed}`);
59
+ }
60
+ return fields.length > 0 ? fields.join(" ") : "actor=<unknown>";
61
+ }
62
+ /**
63
+ * Register a callback that scheduleGatewaySigusr1Restart checks before emitting SIGUSR1.
64
+ * The callback should return the number of pending items (0 = safe to restart).
65
+ */
66
+ export function setPreRestartDeferralCheck(fn) {
67
+ preRestartCheck = fn;
68
+ }
69
+ /**
70
+ * Emit an authorized SIGUSR1 gateway restart, guarded against duplicate emissions.
71
+ * Returns true if SIGUSR1 was emitted, false if a restart was already emitted.
72
+ * Both scheduleGatewaySigusr1Restart and the config watcher should use this
73
+ * to ensure only one restart fires.
74
+ */
75
+ export function emitGatewayRestart() {
76
+ if (hasUnconsumedRestartSignal()) {
77
+ clearPendingScheduledRestart();
78
+ return false;
79
+ }
80
+ clearPendingScheduledRestart();
81
+ const cycleToken = ++restartCycleToken;
82
+ emittedRestartToken = cycleToken;
83
+ authorizeGatewaySigusr1Restart();
84
+ try {
85
+ if (process.listenerCount("SIGUSR1") > 0) {
86
+ process.emit("SIGUSR1");
87
+ }
88
+ else {
89
+ process.kill(process.pid, "SIGUSR1");
90
+ }
91
+ }
92
+ catch {
93
+ // Roll back the cycle marker so future restart requests can still proceed.
94
+ emittedRestartToken = consumedRestartToken;
95
+ return false;
96
+ }
97
+ lastRestartEmittedAt = Date.now();
98
+ return true;
99
+ }
8
100
  function resetSigusr1AuthorizationIfExpired(now = Date.now()) {
9
- if (sigusr1AuthorizedCount <= 0)
101
+ if (sigusr1AuthorizedCount <= 0) {
10
102
  return;
11
- if (now <= sigusr1AuthorizedUntil)
103
+ }
104
+ if (now <= sigusr1AuthorizedUntil) {
12
105
  return;
106
+ }
13
107
  sigusr1AuthorizedCount = 0;
14
108
  sigusr1AuthorizedUntil = 0;
15
109
  }
@@ -19,7 +113,7 @@ export function setGatewaySigusr1RestartPolicy(opts) {
19
113
  export function isGatewaySigusr1RestartExternallyAllowed() {
20
114
  return sigusr1ExternalAllowed;
21
115
  }
22
- export function authorizeGatewaySigusr1Restart(delayMs = 0) {
116
+ function authorizeGatewaySigusr1Restart(delayMs = 0) {
23
117
  const delay = Math.max(0, Math.floor(delayMs));
24
118
  const expiresAt = Date.now() + delay + SIGUSR1_AUTH_GRACE_MS;
25
119
  sigusr1AuthorizedCount += 1;
@@ -29,24 +123,87 @@ export function authorizeGatewaySigusr1Restart(delayMs = 0) {
29
123
  }
30
124
  export function consumeGatewaySigusr1RestartAuthorization() {
31
125
  resetSigusr1AuthorizationIfExpired();
32
- if (sigusr1AuthorizedCount <= 0)
126
+ if (sigusr1AuthorizedCount <= 0) {
33
127
  return false;
128
+ }
34
129
  sigusr1AuthorizedCount -= 1;
35
130
  if (sigusr1AuthorizedCount <= 0) {
36
131
  sigusr1AuthorizedUntil = 0;
37
132
  }
38
133
  return true;
39
134
  }
135
+ /**
136
+ * Mark the currently emitted SIGUSR1 restart cycle as consumed by the run loop.
137
+ * This explicitly advances the cycle state instead of resetting emit guards inside
138
+ * consumeGatewaySigusr1RestartAuthorization().
139
+ */
140
+ export function markGatewaySigusr1RestartHandled() {
141
+ if (hasUnconsumedRestartSignal()) {
142
+ consumedRestartToken = emittedRestartToken;
143
+ }
144
+ }
145
+ /**
146
+ * Poll pending work until it drains (or times out), then emit one restart signal.
147
+ * Shared by both the direct RPC restart path and the config watcher path.
148
+ */
149
+ export function deferGatewayRestartUntilIdle(opts) {
150
+ const pollMsRaw = opts.pollMs ?? DEFAULT_DEFERRAL_POLL_MS;
151
+ const pollMs = Math.max(10, Math.floor(pollMsRaw));
152
+ const maxWaitMsRaw = opts.maxWaitMs ?? DEFAULT_DEFERRAL_MAX_WAIT_MS;
153
+ const maxWaitMs = Math.max(pollMs, Math.floor(maxWaitMsRaw));
154
+ let pending;
155
+ try {
156
+ pending = opts.getPendingCount();
157
+ }
158
+ catch (err) {
159
+ opts.hooks?.onCheckError?.(err);
160
+ emitGatewayRestart();
161
+ return;
162
+ }
163
+ if (pending <= 0) {
164
+ opts.hooks?.onReady?.();
165
+ emitGatewayRestart();
166
+ return;
167
+ }
168
+ opts.hooks?.onDeferring?.(pending);
169
+ const startedAt = Date.now();
170
+ const poll = setInterval(() => {
171
+ let current;
172
+ try {
173
+ current = opts.getPendingCount();
174
+ }
175
+ catch (err) {
176
+ clearInterval(poll);
177
+ opts.hooks?.onCheckError?.(err);
178
+ emitGatewayRestart();
179
+ return;
180
+ }
181
+ if (current <= 0) {
182
+ clearInterval(poll);
183
+ opts.hooks?.onReady?.();
184
+ emitGatewayRestart();
185
+ return;
186
+ }
187
+ const elapsedMs = Date.now() - startedAt;
188
+ if (elapsedMs >= maxWaitMs) {
189
+ clearInterval(poll);
190
+ opts.hooks?.onTimeout?.(current, elapsedMs);
191
+ emitGatewayRestart();
192
+ }
193
+ }, pollMs);
194
+ }
40
195
  function formatSpawnDetail(result) {
41
196
  const clean = (value) => {
42
197
  const text = typeof value === "string" ? value : value ? value.toString() : "";
43
198
  return text.replace(/\s+/g, " ").trim();
44
199
  };
45
200
  if (result.error) {
46
- if (result.error instanceof Error)
201
+ if (result.error instanceof Error) {
47
202
  return result.error.message;
48
- if (typeof result.error === "string")
203
+ }
204
+ if (typeof result.error === "string") {
49
205
  return result.error;
206
+ }
50
207
  try {
51
208
  return JSON.stringify(result.error);
52
209
  }
@@ -55,13 +212,16 @@ function formatSpawnDetail(result) {
55
212
  }
56
213
  }
57
214
  const stderr = clean(result.stderr);
58
- if (stderr)
215
+ if (stderr) {
59
216
  return stderr;
217
+ }
60
218
  const stdout = clean(result.stdout);
61
- if (stdout)
219
+ if (stdout) {
62
220
  return stdout;
63
- if (typeof result.status === "number")
221
+ }
222
+ if (typeof result.status === "number") {
64
223
  return `exit ${result.status}`;
224
+ }
65
225
  return "unknown error";
66
226
  }
67
227
  function normalizeSystemdUnit(raw, profile) {
@@ -138,29 +298,66 @@ export function scheduleGatewaySigusr1Restart(opts) {
138
298
  const reason = typeof opts?.reason === "string" && opts.reason.trim()
139
299
  ? opts.reason.trim().slice(0, 200)
140
300
  : undefined;
141
- authorizeGatewaySigusr1Restart(delayMs);
142
- const pid = process.pid;
143
- const hasListener = process.listenerCount("SIGUSR1") > 0;
144
- setTimeout(() => {
145
- try {
146
- if (hasListener) {
147
- process.emit("SIGUSR1");
148
- }
149
- else {
150
- process.kill(pid, "SIGUSR1");
151
- }
301
+ const mode = process.listenerCount("SIGUSR1") > 0 ? "emit" : "signal";
302
+ const nowMs = Date.now();
303
+ const cooldownMsApplied = Math.max(0, lastRestartEmittedAt + RESTART_COOLDOWN_MS - nowMs);
304
+ const requestedDueAt = nowMs + delayMs + cooldownMsApplied;
305
+ if (hasUnconsumedRestartSignal()) {
306
+ restartLog.warn(`restart request coalesced (already in-flight) reason=${reason ?? "unspecified"} ${formatRestartAudit(opts?.audit)}`);
307
+ return {
308
+ ok: true,
309
+ pid: process.pid,
310
+ signal: "SIGUSR1",
311
+ delayMs: 0,
312
+ reason,
313
+ mode,
314
+ coalesced: true,
315
+ cooldownMsApplied,
316
+ };
317
+ }
318
+ if (pendingRestartTimer) {
319
+ const remainingMs = Math.max(0, pendingRestartDueAt - nowMs);
320
+ const shouldPullEarlier = requestedDueAt < pendingRestartDueAt;
321
+ if (shouldPullEarlier) {
322
+ restartLog.warn(`restart request rescheduled earlier reason=${reason ?? "unspecified"} pendingReason=${pendingRestartReason ?? "unspecified"} oldDelayMs=${remainingMs} newDelayMs=${Math.max(0, requestedDueAt - nowMs)} ${formatRestartAudit(opts?.audit)}`);
323
+ clearPendingScheduledRestart();
152
324
  }
153
- catch {
154
- /* ignore */
325
+ else {
326
+ restartLog.warn(`restart request coalesced (already scheduled) reason=${reason ?? "unspecified"} pendingReason=${pendingRestartReason ?? "unspecified"} delayMs=${remainingMs} ${formatRestartAudit(opts?.audit)}`);
327
+ return {
328
+ ok: true,
329
+ pid: process.pid,
330
+ signal: "SIGUSR1",
331
+ delayMs: remainingMs,
332
+ reason,
333
+ mode,
334
+ coalesced: true,
335
+ cooldownMsApplied,
336
+ };
337
+ }
338
+ }
339
+ pendingRestartDueAt = requestedDueAt;
340
+ pendingRestartReason = reason;
341
+ pendingRestartTimer = setTimeout(() => {
342
+ pendingRestartTimer = null;
343
+ pendingRestartDueAt = 0;
344
+ pendingRestartReason = undefined;
345
+ const pendingCheck = preRestartCheck;
346
+ if (!pendingCheck) {
347
+ emitGatewayRestart();
348
+ return;
155
349
  }
156
- }, delayMs);
350
+ deferGatewayRestartUntilIdle({ getPendingCount: pendingCheck });
351
+ }, Math.max(0, requestedDueAt - nowMs));
157
352
  return {
158
353
  ok: true,
159
- pid,
354
+ pid: process.pid,
160
355
  signal: "SIGUSR1",
161
- delayMs,
356
+ delayMs: Math.max(0, requestedDueAt - nowMs),
162
357
  reason,
163
- mode: hasListener ? "emit" : "signal",
358
+ mode,
359
+ coalesced: false,
360
+ cooldownMsApplied,
164
361
  };
165
362
  }
166
363
  export const __testing = {
@@ -168,5 +365,11 @@ export const __testing = {
168
365
  sigusr1AuthorizedCount = 0;
169
366
  sigusr1AuthorizedUntil = 0;
170
367
  sigusr1ExternalAllowed = false;
368
+ preRestartCheck = null;
369
+ restartCycleToken = 0;
370
+ emittedRestartToken = 0;
371
+ consumedRestartToken = 0;
372
+ lastRestartEmittedAt = 0;
373
+ clearPendingScheduledRestart();
171
374
  },
172
375
  };
@@ -0,0 +1,54 @@
1
+ const SSH_TOKEN = /^[A-Za-z0-9._-]+$/;
2
+ const BRACKETED_IPV6 = /^\[[0-9A-Fa-f:.%]+\]$/;
3
+ const WHITESPACE = /\s/;
4
+ function hasControlOrWhitespace(value) {
5
+ for (const char of value) {
6
+ const code = char.charCodeAt(0);
7
+ if (code <= 0x1f || code === 0x7f || WHITESPACE.test(char)) {
8
+ return true;
9
+ }
10
+ }
11
+ return false;
12
+ }
13
+ export function normalizeScpRemoteHost(value) {
14
+ if (typeof value !== "string") {
15
+ return undefined;
16
+ }
17
+ const trimmed = value.trim();
18
+ if (!trimmed) {
19
+ return undefined;
20
+ }
21
+ if (hasControlOrWhitespace(trimmed)) {
22
+ return undefined;
23
+ }
24
+ if (trimmed.startsWith("-") || trimmed.includes("/") || trimmed.includes("\\")) {
25
+ return undefined;
26
+ }
27
+ const firstAt = trimmed.indexOf("@");
28
+ const lastAt = trimmed.lastIndexOf("@");
29
+ let user;
30
+ let host = trimmed;
31
+ if (firstAt !== -1) {
32
+ if (firstAt !== lastAt || firstAt === 0 || firstAt === trimmed.length - 1) {
33
+ return undefined;
34
+ }
35
+ user = trimmed.slice(0, firstAt);
36
+ host = trimmed.slice(firstAt + 1);
37
+ if (!SSH_TOKEN.test(user)) {
38
+ return undefined;
39
+ }
40
+ }
41
+ if (!host || host.startsWith("-") || host.includes("@")) {
42
+ return undefined;
43
+ }
44
+ if (host.includes(":") && !BRACKETED_IPV6.test(host)) {
45
+ return undefined;
46
+ }
47
+ if (!SSH_TOKEN.test(host) && !BRACKETED_IPV6.test(host)) {
48
+ return undefined;
49
+ }
50
+ return user ? `${user}@${host}` : host;
51
+ }
52
+ export function isSafeScpRemoteHost(value) {
53
+ return normalizeScpRemoteHost(value) !== undefined;
54
+ }
@@ -1,18 +1,27 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { formatCliCommand } from "../cli/command-format.js";
3
4
  import { resolveStateDir } from "../config/paths.js";
5
+ import { VERSION } from "../version.js";
4
6
  import { resolvePoolBotPackageRoot } from "./poolbot-root.js";
5
- import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js";
6
7
  import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js";
7
- import { VERSION } from "../version.js";
8
- import { formatCliCommand } from "../cli/command-format.js";
8
+ import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js";
9
+ let updateAvailableCache = null;
10
+ export function getUpdateAvailable() {
11
+ return updateAvailableCache;
12
+ }
13
+ export function resetUpdateAvailableStateForTest() {
14
+ updateAvailableCache = null;
15
+ }
9
16
  const UPDATE_CHECK_FILENAME = "update-check.json";
10
17
  const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
11
18
  function shouldSkipCheck(allowInTests) {
12
- if (allowInTests)
19
+ if (allowInTests) {
13
20
  return false;
14
- if (process.env.VITEST || process.env.NODE_ENV === "test")
21
+ }
22
+ if (process.env.VITEST || process.env.NODE_ENV === "test") {
15
23
  return true;
24
+ }
16
25
  return false;
17
26
  }
18
27
  async function readState(statePath) {
@@ -29,20 +38,63 @@ async function writeState(statePath, state) {
29
38
  await fs.mkdir(path.dirname(statePath), { recursive: true });
30
39
  await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
31
40
  }
41
+ function sameUpdateAvailable(a, b) {
42
+ if (a === b) {
43
+ return true;
44
+ }
45
+ if (!a || !b) {
46
+ return false;
47
+ }
48
+ return (a.currentVersion === b.currentVersion &&
49
+ a.latestVersion === b.latestVersion &&
50
+ a.channel === b.channel);
51
+ }
52
+ function setUpdateAvailableCache(params) {
53
+ if (sameUpdateAvailable(updateAvailableCache, params.next)) {
54
+ return;
55
+ }
56
+ updateAvailableCache = params.next;
57
+ params.onUpdateAvailableChange?.(params.next);
58
+ }
59
+ function resolvePersistedUpdateAvailable(state) {
60
+ const latestVersion = state.lastAvailableVersion?.trim();
61
+ if (!latestVersion) {
62
+ return null;
63
+ }
64
+ const cmp = compareSemverStrings(VERSION, latestVersion);
65
+ if (cmp == null || cmp >= 0) {
66
+ return null;
67
+ }
68
+ const channel = state.lastAvailableTag?.trim() || DEFAULT_PACKAGE_CHANNEL;
69
+ return {
70
+ currentVersion: VERSION,
71
+ latestVersion,
72
+ channel,
73
+ };
74
+ }
32
75
  export async function runGatewayUpdateCheck(params) {
33
- if (shouldSkipCheck(Boolean(params.allowInTests)))
76
+ if (shouldSkipCheck(Boolean(params.allowInTests))) {
34
77
  return;
35
- if (params.isNixMode)
78
+ }
79
+ if (params.isNixMode) {
36
80
  return;
37
- if (params.cfg.update?.checkOnStart === false)
81
+ }
82
+ if (params.cfg.update?.checkOnStart === false) {
38
83
  return;
84
+ }
39
85
  const statePath = path.join(resolveStateDir(), UPDATE_CHECK_FILENAME);
40
86
  const state = await readState(statePath);
41
87
  const now = Date.now();
42
88
  const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
89
+ const persistedAvailable = resolvePersistedUpdateAvailable(state);
90
+ setUpdateAvailableCache({
91
+ next: persistedAvailable,
92
+ onUpdateAvailableChange: params.onUpdateAvailableChange,
93
+ });
43
94
  if (lastCheckedAt && Number.isFinite(lastCheckedAt)) {
44
- if (now - lastCheckedAt < UPDATE_CHECK_INTERVAL_MS)
95
+ if (now - lastCheckedAt < UPDATE_CHECK_INTERVAL_MS) {
45
96
  return;
97
+ }
46
98
  }
47
99
  const root = await resolvePoolBotPackageRoot({
48
100
  moduleUrl: import.meta.url,
@@ -60,6 +112,12 @@ export async function runGatewayUpdateCheck(params) {
60
112
  lastCheckedAt: new Date(now).toISOString(),
61
113
  };
62
114
  if (status.installKind !== "package") {
115
+ delete nextState.lastAvailableVersion;
116
+ delete nextState.lastAvailableTag;
117
+ setUpdateAvailableCache({
118
+ next: null,
119
+ onUpdateAvailableChange: params.onUpdateAvailableChange,
120
+ });
63
121
  await writeState(statePath, nextState);
64
122
  return;
65
123
  }
@@ -72,6 +130,17 @@ export async function runGatewayUpdateCheck(params) {
72
130
  }
73
131
  const cmp = compareSemverStrings(VERSION, resolved.version);
74
132
  if (cmp != null && cmp < 0) {
133
+ const nextAvailable = {
134
+ currentVersion: VERSION,
135
+ latestVersion: resolved.version,
136
+ channel: tag,
137
+ };
138
+ setUpdateAvailableCache({
139
+ next: nextAvailable,
140
+ onUpdateAvailableChange: params.onUpdateAvailableChange,
141
+ });
142
+ nextState.lastAvailableVersion = resolved.version;
143
+ nextState.lastAvailableTag = tag;
75
144
  const shouldNotify = state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag;
76
145
  if (shouldNotify) {
77
146
  params.log.info(`update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("poolbot update")}`);
@@ -79,6 +148,14 @@ export async function runGatewayUpdateCheck(params) {
79
148
  nextState.lastNotifiedTag = tag;
80
149
  }
81
150
  }
151
+ else {
152
+ delete nextState.lastAvailableVersion;
153
+ delete nextState.lastAvailableTag;
154
+ setUpdateAvailableCache({
155
+ next: null,
156
+ onUpdateAvailableChange: params.onUpdateAvailableChange,
157
+ });
158
+ }
82
159
  await writeState(statePath, nextState);
83
160
  }
84
161
  export function scheduleGatewayUpdateCheck(params) {
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ const WILDCARD_SEGMENT = "*";
3
+ const WINDOWS_DRIVE_ABS_RE = /^[A-Za-z]:\//;
4
+ const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:$/;
5
+ export const DEFAULT_IMESSAGE_ATTACHMENT_ROOTS = ["/Users/*/Library/Messages/Attachments"];
6
+ function normalizePosixAbsolutePath(value) {
7
+ const trimmed = value.trim();
8
+ if (!trimmed || trimmed.includes("\0")) {
9
+ return undefined;
10
+ }
11
+ const normalized = path.posix.normalize(trimmed.replaceAll("\\", "/"));
12
+ const isAbsolute = normalized.startsWith("/") || WINDOWS_DRIVE_ABS_RE.test(normalized);
13
+ if (!isAbsolute || normalized === "/") {
14
+ return undefined;
15
+ }
16
+ const withoutTrailingSlash = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
17
+ if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
18
+ return undefined;
19
+ }
20
+ return withoutTrailingSlash;
21
+ }
22
+ function splitPathSegments(value) {
23
+ return value.split("/").filter(Boolean);
24
+ }
25
+ function matchesRootPattern(params) {
26
+ const candidateSegments = splitPathSegments(params.candidatePath);
27
+ const rootSegments = splitPathSegments(params.rootPattern);
28
+ if (candidateSegments.length < rootSegments.length) {
29
+ return false;
30
+ }
31
+ for (let idx = 0; idx < rootSegments.length; idx += 1) {
32
+ const expected = rootSegments[idx];
33
+ const actual = candidateSegments[idx];
34
+ if (expected === WILDCARD_SEGMENT) {
35
+ continue;
36
+ }
37
+ if (expected !== actual) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ export function isValidInboundPathRootPattern(value) {
44
+ const normalized = normalizePosixAbsolutePath(value);
45
+ if (!normalized) {
46
+ return false;
47
+ }
48
+ const segments = splitPathSegments(normalized);
49
+ if (segments.length === 0) {
50
+ return false;
51
+ }
52
+ return segments.every((segment) => segment === WILDCARD_SEGMENT || !segment.includes("*"));
53
+ }
54
+ export function normalizeInboundPathRoots(roots) {
55
+ const normalized = [];
56
+ const seen = new Set();
57
+ for (const root of roots ?? []) {
58
+ if (typeof root !== "string") {
59
+ continue;
60
+ }
61
+ if (!isValidInboundPathRootPattern(root)) {
62
+ continue;
63
+ }
64
+ const candidate = normalizePosixAbsolutePath(root);
65
+ if (!candidate || seen.has(candidate)) {
66
+ continue;
67
+ }
68
+ seen.add(candidate);
69
+ normalized.push(candidate);
70
+ }
71
+ return normalized;
72
+ }
73
+ export function mergeInboundPathRoots(...rootsLists) {
74
+ const merged = [];
75
+ const seen = new Set();
76
+ for (const roots of rootsLists) {
77
+ const normalized = normalizeInboundPathRoots(roots);
78
+ for (const root of normalized) {
79
+ if (seen.has(root)) {
80
+ continue;
81
+ }
82
+ seen.add(root);
83
+ merged.push(root);
84
+ }
85
+ }
86
+ return merged;
87
+ }
88
+ export function isInboundPathAllowed(params) {
89
+ const candidatePath = normalizePosixAbsolutePath(params.filePath);
90
+ if (!candidatePath) {
91
+ return false;
92
+ }
93
+ const roots = normalizeInboundPathRoots(params.roots);
94
+ const effectiveRoots = roots.length > 0 ? roots : normalizeInboundPathRoots(params.fallbackRoots ?? undefined);
95
+ if (effectiveRoots.length === 0) {
96
+ return false;
97
+ }
98
+ return effectiveRoots.some((rootPattern) => matchesRootPattern({ candidatePath, rootPattern }));
99
+ }
100
+ function resolveIMessageAccountConfig(params) {
101
+ const accountId = params.accountId?.trim();
102
+ if (!accountId) {
103
+ return undefined;
104
+ }
105
+ return params.cfg.channels?.imessage?.accounts?.[accountId];
106
+ }
107
+ export function resolveIMessageAttachmentRoots(params) {
108
+ const accountConfig = resolveIMessageAccountConfig(params);
109
+ return mergeInboundPathRoots(accountConfig?.attachmentRoots, params.cfg.channels?.imessage?.attachmentRoots, DEFAULT_IMESSAGE_ATTACHMENT_ROOTS);
110
+ }
111
+ export function resolveIMessageRemoteAttachmentRoots(params) {
112
+ const accountConfig = resolveIMessageAccountConfig(params);
113
+ return mergeInboundPathRoots(accountConfig?.remoteAttachmentRoots, params.cfg.channels?.imessage?.remoteAttachmentRoots, accountConfig?.attachmentRoots, params.cfg.channels?.imessage?.attachmentRoots, DEFAULT_IMESSAGE_ATTACHMENT_ROOTS);
114
+ }
@@ -62,6 +62,22 @@ export function normalizeMimeList(values, fallback) {
62
62
  const input = values && values.length > 0 ? values : fallback;
63
63
  return new Set(input.map((value) => normalizeMimeType(value)).filter(Boolean));
64
64
  }
65
+ export function resolveInputFileLimits(config) {
66
+ return {
67
+ allowUrl: config?.allowUrl ?? true,
68
+ allowedMimes: normalizeMimeList(config?.allowedMimes, DEFAULT_INPUT_FILE_MIMES),
69
+ maxBytes: config?.maxBytes ?? DEFAULT_INPUT_FILE_MAX_BYTES,
70
+ maxChars: config?.maxChars ?? DEFAULT_INPUT_FILE_MAX_CHARS,
71
+ maxRedirects: config?.maxRedirects ?? DEFAULT_INPUT_MAX_REDIRECTS,
72
+ timeoutMs: config?.timeoutMs ?? DEFAULT_INPUT_TIMEOUT_MS,
73
+ urlAllowlist: config?.urlAllowlist,
74
+ pdf: {
75
+ maxPages: config?.pdf?.maxPages ?? DEFAULT_INPUT_PDF_MAX_PAGES,
76
+ maxPixels: config?.pdf?.maxPixels ?? DEFAULT_INPUT_PDF_MAX_PIXELS,
77
+ minTextChars: config?.pdf?.minTextChars ?? DEFAULT_INPUT_PDF_MIN_TEXT_CHARS,
78
+ },
79
+ };
80
+ }
65
81
  export async function fetchWithGuard(params) {
66
82
  let currentUrl = params.url;
67
83
  let redirectCount = 0;
@@ -0,0 +1,8 @@
1
+ import { getMemorySearchManager } from "./index.js";
2
+ export async function createMemoryManagerOrThrow(cfg, agentId = "main") {
3
+ const result = await getMemorySearchManager({ cfg, agentId });
4
+ if (!result.manager) {
5
+ throw new Error("manager missing");
6
+ }
7
+ return result.manager;
8
+ }