@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
@@ -0,0 +1,126 @@
1
+ import crypto from "node:crypto";
2
+ import { writeConfigFile } from "../config/config.js";
3
+ import { resolveGatewayAuth } from "./auth.js";
4
+ export function mergeGatewayAuthConfig(base, override) {
5
+ const merged = { ...base };
6
+ if (!override) {
7
+ return merged;
8
+ }
9
+ if (override.mode !== undefined) {
10
+ merged.mode = override.mode;
11
+ }
12
+ if (override.token !== undefined) {
13
+ merged.token = override.token;
14
+ }
15
+ if (override.password !== undefined) {
16
+ merged.password = override.password;
17
+ }
18
+ if (override.allowTailscale !== undefined) {
19
+ merged.allowTailscale = override.allowTailscale;
20
+ }
21
+ if (override.rateLimit !== undefined) {
22
+ merged.rateLimit = override.rateLimit;
23
+ }
24
+ if (override.trustedProxy !== undefined) {
25
+ merged.trustedProxy = override.trustedProxy;
26
+ }
27
+ return merged;
28
+ }
29
+ export function mergeGatewayTailscaleConfig(base, override) {
30
+ const merged = { ...base };
31
+ if (!override) {
32
+ return merged;
33
+ }
34
+ if (override.mode !== undefined) {
35
+ merged.mode = override.mode;
36
+ }
37
+ if (override.resetOnExit !== undefined) {
38
+ merged.resetOnExit = override.resetOnExit;
39
+ }
40
+ return merged;
41
+ }
42
+ function resolveGatewayAuthFromConfig(params) {
43
+ const tailscaleConfig = mergeGatewayTailscaleConfig(params.cfg.gateway?.tailscale, params.tailscaleOverride);
44
+ return resolveGatewayAuth({
45
+ authConfig: params.cfg.gateway?.auth,
46
+ authOverride: params.authOverride,
47
+ env: params.env,
48
+ tailscaleMode: tailscaleConfig.mode ?? "off",
49
+ });
50
+ }
51
+ function shouldPersistGeneratedToken(params) {
52
+ if (!params.persistRequested) {
53
+ return false;
54
+ }
55
+ // Keep CLI/runtime mode overrides ephemeral: startup should not silently
56
+ // mutate durable auth policy when mode was chosen by an override flag.
57
+ if (params.resolvedAuth.modeSource === "override") {
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+ export async function ensureGatewayStartupAuth(params) {
63
+ const env = params.env ?? process.env;
64
+ const persistRequested = params.persist === true;
65
+ const resolved = resolveGatewayAuthFromConfig({
66
+ cfg: params.cfg,
67
+ env,
68
+ authOverride: params.authOverride,
69
+ tailscaleOverride: params.tailscaleOverride,
70
+ });
71
+ if (resolved.mode !== "token" || (resolved.token?.trim().length ?? 0) > 0) {
72
+ assertHooksTokenSeparateFromGatewayAuth({ cfg: params.cfg, auth: resolved });
73
+ return { cfg: params.cfg, auth: resolved, persistedGeneratedToken: false };
74
+ }
75
+ const generatedToken = crypto.randomBytes(24).toString("hex");
76
+ const nextCfg = {
77
+ ...params.cfg,
78
+ gateway: {
79
+ ...params.cfg.gateway,
80
+ auth: {
81
+ ...params.cfg.gateway?.auth,
82
+ mode: "token",
83
+ token: generatedToken,
84
+ },
85
+ },
86
+ };
87
+ const persist = shouldPersistGeneratedToken({
88
+ persistRequested,
89
+ resolvedAuth: resolved,
90
+ });
91
+ if (persist) {
92
+ await writeConfigFile(nextCfg);
93
+ }
94
+ const nextAuth = resolveGatewayAuthFromConfig({
95
+ cfg: nextCfg,
96
+ env,
97
+ authOverride: params.authOverride,
98
+ tailscaleOverride: params.tailscaleOverride,
99
+ });
100
+ assertHooksTokenSeparateFromGatewayAuth({ cfg: nextCfg, auth: nextAuth });
101
+ return {
102
+ cfg: nextCfg,
103
+ auth: nextAuth,
104
+ generatedToken,
105
+ persistedGeneratedToken: persist,
106
+ };
107
+ }
108
+ export function assertHooksTokenSeparateFromGatewayAuth(params) {
109
+ if (params.cfg.hooks?.enabled !== true) {
110
+ return;
111
+ }
112
+ const hooksToken = typeof params.cfg.hooks.token === "string" ? params.cfg.hooks.token.trim() : "";
113
+ if (!hooksToken) {
114
+ return;
115
+ }
116
+ const gatewayToken = params.auth.mode === "token" && typeof params.auth.token === "string"
117
+ ? params.auth.token.trim()
118
+ : "";
119
+ if (!gatewayToken) {
120
+ return;
121
+ }
122
+ if (hooksToken !== gatewayToken) {
123
+ return;
124
+ }
125
+ throw new Error("Invalid config: hooks.token must not match gateway auth token. Set a distinct hooks.token for hook ingress.");
126
+ }
@@ -0,0 +1,15 @@
1
+ export function extractPayloadText(result) {
2
+ const record = result;
3
+ const payloads = Array.isArray(record.payloads) ? record.payloads : [];
4
+ const texts = payloads
5
+ .map((p) => (p && typeof p === "object" ? p.text : undefined))
6
+ .filter((t) => typeof t === "string" && t.trim().length > 0);
7
+ return texts.join("\n").trim();
8
+ }
9
+ export function buildAssistantDeltaResult(params) {
10
+ const runId = params.opts?.runId ?? "";
11
+ for (const delta of params.deltas) {
12
+ params.emit({ runId, stream: "assistant", data: { delta } });
13
+ }
14
+ return { payloads: [{ text: params.text }] };
15
+ }
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
2
  import fsSync from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { vi } from "vitest";
@@ -147,6 +147,7 @@ const hoisted = vi.hoisted(() => ({
147
147
  waitCalls: [],
148
148
  waitResults: new Map(),
149
149
  },
150
+ testTailscaleWhois: { value: null },
150
151
  getReplyFromConfig: vi.fn().mockResolvedValue(undefined),
151
152
  sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
152
153
  }));
@@ -168,9 +169,9 @@ const testConfigRoot = {
168
169
  export const setTestConfigRoot = (root) => {
169
170
  testConfigRoot.value = root;
170
171
  process.env.POOLBOT_CONFIG_PATH = path.join(root, "poolbot.json");
171
- process.env.CLAWDBOT_CONFIG_PATH = path.join(root, "poolbot.json");
172
172
  };
173
173
  export const testTailnetIPv4 = hoisted.testTailnetIPv4;
174
+ export const testTailscaleWhois = hoisted.testTailscaleWhois;
174
175
  export const piSdkMock = hoisted.piSdkMock;
175
176
  export const cronIsolatedRun = hoisted.cronIsolatedRun;
176
177
  export const agentCommand = hoisted.agentCommand;
@@ -222,6 +223,13 @@ vi.mock("../infra/tailnet.js", () => ({
222
223
  pickPrimaryTailnetIPv4: () => testTailnetIPv4.value,
223
224
  pickPrimaryTailnetIPv6: () => undefined,
224
225
  }));
226
+ vi.mock("../infra/tailscale.js", async () => {
227
+ const actual = await vi.importActual("../infra/tailscale.js");
228
+ return {
229
+ ...actual,
230
+ readTailscaleWhoisIdentity: async () => testTailscaleWhois.value,
231
+ };
232
+ });
225
233
  vi.mock("../config/sessions.js", async () => {
226
234
  const actual = await vi.importActual("../config/sessions.js");
227
235
  return {
@@ -350,8 +358,8 @@ vi.mock("../config/config.js", async () => {
350
358
  ? fileAgents.defaults
351
359
  : {};
352
360
  const defaults = {
353
- model: { primary: "anthropic/claude-opus-4-5" },
354
- workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
361
+ model: { primary: "anthropic/claude-opus-4-6" },
362
+ workspace: path.join(os.tmpdir(), "poolbot-gateway-test"),
355
363
  ...fileDefaults,
356
364
  ...testState.agentConfig,
357
365
  };
@@ -391,38 +399,46 @@ vi.mock("../config/config.js", async () => {
391
399
  ...fileSession,
392
400
  mainKey: fileSession.mainKey ?? "main",
393
401
  };
394
- if (typeof testState.sessionStorePath === "string")
402
+ if (typeof testState.sessionStorePath === "string") {
395
403
  session.store = testState.sessionStorePath;
396
- if (testState.sessionConfig)
404
+ }
405
+ if (testState.sessionConfig) {
397
406
  Object.assign(session, testState.sessionConfig);
407
+ }
398
408
  const fileGateway = fileConfig.gateway &&
399
409
  typeof fileConfig.gateway === "object" &&
400
410
  !Array.isArray(fileConfig.gateway)
401
411
  ? { ...fileConfig.gateway }
402
412
  : {};
403
- if (testState.gatewayBind)
413
+ if (testState.gatewayBind) {
404
414
  fileGateway.bind = testState.gatewayBind;
405
- if (testState.gatewayAuth)
415
+ }
416
+ if (testState.gatewayAuth) {
406
417
  fileGateway.auth = testState.gatewayAuth;
407
- if (testState.gatewayControlUi)
418
+ }
419
+ if (testState.gatewayControlUi) {
408
420
  fileGateway.controlUi = testState.gatewayControlUi;
421
+ }
409
422
  const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined;
410
423
  const fileCanvasHost = fileConfig.canvasHost &&
411
424
  typeof fileConfig.canvasHost === "object" &&
412
425
  !Array.isArray(fileConfig.canvasHost)
413
426
  ? { ...fileConfig.canvasHost }
414
427
  : {};
415
- if (typeof testState.canvasHostPort === "number")
428
+ if (typeof testState.canvasHostPort === "number") {
416
429
  fileCanvasHost.port = testState.canvasHostPort;
430
+ }
417
431
  const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined;
418
432
  const hooks = testState.hooksConfig ?? fileConfig.hooks;
419
433
  const fileCron = fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron)
420
434
  ? { ...fileConfig.cron }
421
435
  : {};
422
- if (typeof testState.cronEnabled === "boolean")
436
+ if (typeof testState.cronEnabled === "boolean") {
423
437
  fileCron.enabled = testState.cronEnabled;
424
- if (typeof testState.cronStorePath === "string")
438
+ }
439
+ if (typeof testState.cronStorePath === "string") {
425
440
  fileCron.store = testState.cronStorePath;
441
+ }
426
442
  const cron = Object.keys(fileCron).length > 0 ? fileCron : undefined;
427
443
  const config = {
428
444
  ...fileConfig,
@@ -503,7 +519,14 @@ vi.mock("../cli/deps.js", async () => {
503
519
  }),
504
520
  };
505
521
  });
522
+ vi.mock("../plugins/loader.js", async () => {
523
+ const actual = await vi.importActual("../plugins/loader.js");
524
+ return {
525
+ ...actual,
526
+ loadPoolBotPlugins: () => pluginRegistryState.registry,
527
+ };
528
+ });
529
+ process.env.POOLBOT_SKIP_CHANNELS = "1";
530
+ process.env.POOLBOT_SKIP_CRON = "1";
506
531
  process.env.POOLBOT_SKIP_CHANNELS = "1";
507
- process.env.CLAWDBOT_SKIP_CHANNELS = "1";
508
532
  process.env.POOLBOT_SKIP_CRON = "1";
509
- process.env.CLAWDBOT_SKIP_CRON = "1";
@@ -11,15 +11,19 @@ import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
11
11
  import { rawDataToString } from "../infra/ws.js";
12
12
  import { resetLogger, setLoggerOverride } from "../logging.js";
13
13
  import { DEFAULT_AGENT_ID, toAgentStoreSessionKey } from "../routing/session-key.js";
14
- import { getDeterministicFreePortBlock } from "../test-utils/ports.js";
15
14
  import { captureEnv } from "../test-utils/env.js";
15
+ import { getDeterministicFreePortBlock } from "../test-utils/ports.js";
16
16
  import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
17
- import { PROTOCOL_VERSION } from "./protocol/index.js";
18
17
  import { buildDeviceAuthPayload } from "./device-auth.js";
19
- import { agentCommand, cronIsolatedRun, embeddedRunMock, piSdkMock, sessionStoreSaveDelayMs, setTestConfigRoot, testIsNixMode, testState, testTailnetIPv4, } from "./test-helpers.mocks.js";
20
- // Preload the gateway server module once per worker.
21
- // Important: `test-helpers.mocks` must run before importing the server so vi.mock hooks apply.
22
- const serverModulePromise = import("./server.js");
18
+ import { PROTOCOL_VERSION } from "./protocol/index.js";
19
+ import { agentCommand, cronIsolatedRun, embeddedRunMock, piSdkMock, sessionStoreSaveDelayMs, setTestConfigRoot, testIsNixMode, testTailscaleWhois, testState, testTailnetIPv4, } from "./test-helpers.mocks.js";
20
+ // Import lazily after test env/home setup so config/session paths resolve to test dirs.
21
+ // Keep one cached module per worker for speed.
22
+ let serverModulePromise;
23
+ async function getServerModule() {
24
+ serverModulePromise ??= import("./server.js");
25
+ return await serverModulePromise;
26
+ }
23
27
  let previousHome;
24
28
  let previousUserProfile;
25
29
  let previousStateDir;
@@ -28,12 +32,17 @@ let previousSkipBrowserControl;
28
32
  let previousSkipGmailWatcher;
29
33
  let previousSkipCanvasHost;
30
34
  let previousBundledPluginsDir;
35
+ let previousSkipChannels;
36
+ let previousSkipProviders;
37
+ let previousSkipCron;
38
+ let previousMinimalGateway;
31
39
  let tempHome;
32
40
  let tempConfigRoot;
33
41
  export async function writeSessionStore(params) {
34
42
  const storePath = params.storePath ?? testState.sessionStorePath;
35
- if (!storePath)
43
+ if (!storePath) {
36
44
  throw new Error("writeSessionStore requires testState.sessionStorePath");
45
+ }
37
46
  const agentId = params.agentId ?? DEFAULT_AGENT_ID;
38
47
  const store = {};
39
48
  for (const [requestKey, entry] of Object.entries(params.entries)) {
@@ -53,37 +62,33 @@ export async function writeSessionStore(params) {
53
62
  async function setupGatewayTestHome() {
54
63
  previousHome = process.env.HOME;
55
64
  previousUserProfile = process.env.USERPROFILE;
56
- previousStateDir = process.env.POOLBOT_STATE_DIR ?? process.env.CLAWDBOT_STATE_DIR;
57
- previousConfigPath = process.env.POOLBOT_CONFIG_PATH ?? process.env.CLAWDBOT_CONFIG_PATH;
58
- previousSkipBrowserControl =
59
- process.env.POOLBOT_SKIP_BROWSER_CONTROL_SERVER ??
60
- process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER;
61
- previousSkipGmailWatcher =
62
- process.env.POOLBOT_SKIP_GMAIL_WATCHER ?? process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
63
- previousSkipCanvasHost =
64
- process.env.POOLBOT_SKIP_CANVAS_HOST ?? process.env.CLAWDBOT_SKIP_CANVAS_HOST;
65
- previousBundledPluginsDir =
66
- process.env.POOLBOT_BUNDLED_PLUGINS_DIR ?? process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR;
65
+ previousStateDir = process.env.POOLBOT_STATE_DIR;
66
+ previousConfigPath = process.env.POOLBOT_CONFIG_PATH;
67
+ previousSkipBrowserControl = process.env.POOLBOT_SKIP_BROWSER_CONTROL_SERVER;
68
+ previousSkipGmailWatcher = process.env.POOLBOT_SKIP_GMAIL_WATCHER;
69
+ previousSkipCanvasHost = process.env.POOLBOT_SKIP_CANVAS_HOST;
70
+ previousBundledPluginsDir = process.env.POOLBOT_BUNDLED_PLUGINS_DIR;
71
+ previousSkipChannels = process.env.POOLBOT_SKIP_CHANNELS;
72
+ previousSkipProviders = process.env.POOLBOT_SKIP_PROVIDERS;
73
+ previousSkipCron = process.env.POOLBOT_SKIP_CRON;
74
+ previousMinimalGateway = process.env.POOLBOT_TEST_MINIMAL_GATEWAY;
67
75
  tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "poolbot-gateway-home-"));
68
76
  process.env.HOME = tempHome;
69
77
  process.env.USERPROFILE = tempHome;
70
78
  process.env.POOLBOT_STATE_DIR = path.join(tempHome, ".poolbot");
71
- process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".poolbot");
72
79
  delete process.env.POOLBOT_CONFIG_PATH;
73
- delete process.env.CLAWDBOT_CONFIG_PATH;
74
80
  }
75
81
  function applyGatewaySkipEnv() {
76
82
  process.env.POOLBOT_SKIP_BROWSER_CONTROL_SERVER = "1";
77
- process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1";
78
83
  process.env.POOLBOT_SKIP_GMAIL_WATCHER = "1";
79
- process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
80
84
  process.env.POOLBOT_SKIP_CANVAS_HOST = "1";
81
- process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
82
- const bundledDir = tempHome
85
+ process.env.POOLBOT_SKIP_CHANNELS = "1";
86
+ process.env.POOLBOT_SKIP_PROVIDERS = "1";
87
+ process.env.POOLBOT_SKIP_CRON = "1";
88
+ process.env.POOLBOT_TEST_MINIMAL_GATEWAY = "1";
89
+ process.env.POOLBOT_BUNDLED_PLUGINS_DIR = tempHome
83
90
  ? path.join(tempHome, "poolbot-test-no-bundled-extensions")
84
91
  : "poolbot-test-no-bundled-extensions";
85
- process.env.POOLBOT_BUNDLED_PLUGINS_DIR = bundledDir;
86
- process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
87
92
  }
88
93
  async function resetGatewayTestState(options) {
89
94
  // Some tests intentionally use fake timers; ensure they don't leak into gateway suites.
@@ -93,12 +98,18 @@ async function resetGatewayTestState(options) {
93
98
  throw new Error("resetGatewayTestState called before temp home was initialized");
94
99
  }
95
100
  applyGatewaySkipEnv();
96
- tempConfigRoot = options.uniqueConfigRoot
97
- ? await fs.mkdtemp(path.join(tempHome, "poolbot-test-"))
98
- : path.join(tempHome, ".poolbot-test");
101
+ if (options.uniqueConfigRoot) {
102
+ tempConfigRoot = await fs.mkdtemp(path.join(tempHome, "poolbot-test-"));
103
+ }
104
+ else {
105
+ tempConfigRoot = path.join(tempHome, ".poolbot-test");
106
+ await fs.rm(tempConfigRoot, { recursive: true, force: true });
107
+ await fs.mkdir(tempConfigRoot, { recursive: true });
108
+ }
99
109
  setTestConfigRoot(tempConfigRoot);
100
110
  sessionStoreSaveDelayMs.value = 0;
101
111
  testTailnetIPv4.value = undefined;
112
+ testTailscaleWhois.value = null;
102
113
  testState.gatewayBind = undefined;
103
114
  testState.gatewayAuth = { mode: "token", token: "test-gateway-token-1234567890" };
104
115
  testState.gatewayControlUi = undefined;
@@ -126,7 +137,7 @@ async function resetGatewayTestState(options) {
126
137
  embeddedRunMock.waitResults.clear();
127
138
  drainSystemEvents(resolveMainSessionKeyFromConfig());
128
139
  resetAgentRunContextForTest();
129
- const mod = await serverModulePromise;
140
+ const mod = await getServerModule();
130
141
  mod.__resetModelCatalogCacheForTest();
131
142
  piSdkMock.enabled = false;
132
143
  piSdkMock.discoverCalls = 0;
@@ -136,61 +147,77 @@ async function cleanupGatewayTestHome(options) {
136
147
  vi.useRealTimers();
137
148
  resetLogger();
138
149
  if (options.restoreEnv) {
139
- if (previousHome === undefined)
150
+ if (previousHome === undefined) {
140
151
  delete process.env.HOME;
141
- else
152
+ }
153
+ else {
142
154
  process.env.HOME = previousHome;
143
- if (previousUserProfile === undefined)
155
+ }
156
+ if (previousUserProfile === undefined) {
144
157
  delete process.env.USERPROFILE;
145
- else
158
+ }
159
+ else {
146
160
  process.env.USERPROFILE = previousUserProfile;
161
+ }
147
162
  if (previousStateDir === undefined) {
148
163
  delete process.env.POOLBOT_STATE_DIR;
149
- delete process.env.CLAWDBOT_STATE_DIR;
150
164
  }
151
165
  else {
152
166
  process.env.POOLBOT_STATE_DIR = previousStateDir;
153
- process.env.CLAWDBOT_STATE_DIR = previousStateDir;
154
167
  }
155
168
  if (previousConfigPath === undefined) {
156
169
  delete process.env.POOLBOT_CONFIG_PATH;
157
- delete process.env.CLAWDBOT_CONFIG_PATH;
158
170
  }
159
171
  else {
160
172
  process.env.POOLBOT_CONFIG_PATH = previousConfigPath;
161
- process.env.CLAWDBOT_CONFIG_PATH = previousConfigPath;
162
173
  }
163
174
  if (previousSkipBrowserControl === undefined) {
164
175
  delete process.env.POOLBOT_SKIP_BROWSER_CONTROL_SERVER;
165
- delete process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER;
166
176
  }
167
177
  else {
168
178
  process.env.POOLBOT_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl;
169
- process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl;
170
179
  }
171
180
  if (previousSkipGmailWatcher === undefined) {
172
181
  delete process.env.POOLBOT_SKIP_GMAIL_WATCHER;
173
- delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
174
182
  }
175
183
  else {
176
184
  process.env.POOLBOT_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher;
177
- process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher;
178
185
  }
179
186
  if (previousSkipCanvasHost === undefined) {
180
187
  delete process.env.POOLBOT_SKIP_CANVAS_HOST;
181
- delete process.env.CLAWDBOT_SKIP_CANVAS_HOST;
182
188
  }
183
189
  else {
184
190
  process.env.POOLBOT_SKIP_CANVAS_HOST = previousSkipCanvasHost;
185
- process.env.CLAWDBOT_SKIP_CANVAS_HOST = previousSkipCanvasHost;
186
191
  }
187
192
  if (previousBundledPluginsDir === undefined) {
188
193
  delete process.env.POOLBOT_BUNDLED_PLUGINS_DIR;
189
- delete process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR;
190
194
  }
191
195
  else {
192
196
  process.env.POOLBOT_BUNDLED_PLUGINS_DIR = previousBundledPluginsDir;
193
- process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = previousBundledPluginsDir;
197
+ }
198
+ if (previousSkipChannels === undefined) {
199
+ delete process.env.POOLBOT_SKIP_CHANNELS;
200
+ }
201
+ else {
202
+ process.env.POOLBOT_SKIP_CHANNELS = previousSkipChannels;
203
+ }
204
+ if (previousSkipProviders === undefined) {
205
+ delete process.env.POOLBOT_SKIP_PROVIDERS;
206
+ }
207
+ else {
208
+ process.env.POOLBOT_SKIP_PROVIDERS = previousSkipProviders;
209
+ }
210
+ if (previousSkipCron === undefined) {
211
+ delete process.env.POOLBOT_SKIP_CRON;
212
+ }
213
+ else {
214
+ process.env.POOLBOT_SKIP_CRON = previousSkipCron;
215
+ }
216
+ if (previousMinimalGateway === undefined) {
217
+ delete process.env.POOLBOT_TEST_MINIMAL_GATEWAY;
218
+ }
219
+ else {
220
+ process.env.POOLBOT_TEST_MINIMAL_GATEWAY = previousMinimalGateway;
194
221
  }
195
222
  }
196
223
  if (options.restoreEnv && tempHome) {
@@ -269,14 +296,46 @@ timeoutMs = 10_000) {
269
296
  });
270
297
  }
271
298
  export async function startGatewayServer(port, opts) {
272
- const mod = await serverModulePromise;
299
+ const mod = await getServerModule();
273
300
  const resolvedOpts = opts?.controlUiEnabled === undefined ? { ...opts, controlUiEnabled: false } : opts;
274
301
  return await mod.startGatewayServer(port, resolvedOpts);
275
302
  }
303
+ async function startGatewayServerWithRetries(params) {
304
+ let port = params.port;
305
+ for (let attempt = 0; attempt < 10; attempt++) {
306
+ try {
307
+ return {
308
+ port,
309
+ server: await startGatewayServer(port, params.opts),
310
+ };
311
+ }
312
+ catch (err) {
313
+ const code = err.cause?.code;
314
+ if (code !== "EADDRINUSE") {
315
+ throw err;
316
+ }
317
+ port = await getFreePort();
318
+ }
319
+ }
320
+ throw new Error("failed to start gateway server after retries");
321
+ }
322
+ export async function withGatewayServer(fn, opts) {
323
+ const started = await startGatewayServerWithRetries({
324
+ port: opts?.port ?? (await getFreePort()),
325
+ opts: opts?.serverOptions,
326
+ });
327
+ try {
328
+ return await fn({ port: started.port, server: started.server });
329
+ }
330
+ finally {
331
+ await started.server.close();
332
+ }
333
+ }
276
334
  export async function startServerWithClient(token, opts) {
335
+ const { wsHeaders, ...gatewayOpts } = opts ?? {};
277
336
  let port = await getFreePort();
278
- const envSnapshot = captureEnv(["POOLBOT_GATEWAY_TOKEN", "CLAWDBOT_GATEWAY_TOKEN"]);
279
- const prev = process.env.POOLBOT_GATEWAY_TOKEN ?? process.env.CLAWDBOT_GATEWAY_TOKEN;
337
+ const envSnapshot = captureEnv(["POOLBOT_GATEWAY_TOKEN"]);
338
+ const prev = process.env.POOLBOT_GATEWAY_TOKEN;
280
339
  if (typeof token === "string") {
281
340
  testState.gatewayAuth = { mode: "token", token };
282
341
  }
@@ -286,29 +345,14 @@ export async function startServerWithClient(token, opts) {
286
345
  : undefined);
287
346
  if (fallbackToken === undefined) {
288
347
  delete process.env.POOLBOT_GATEWAY_TOKEN;
289
- delete process.env.CLAWDBOT_GATEWAY_TOKEN;
290
348
  }
291
349
  else {
292
350
  process.env.POOLBOT_GATEWAY_TOKEN = fallbackToken;
293
- process.env.CLAWDBOT_GATEWAY_TOKEN = fallbackToken;
294
351
  }
295
- let server = null;
296
- for (let attempt = 0; attempt < 10; attempt++) {
297
- try {
298
- server = await startGatewayServer(port, opts);
299
- break;
300
- }
301
- catch (err) {
302
- const code = err.cause?.code;
303
- if (code !== "EADDRINUSE")
304
- throw err;
305
- port = await getFreePort();
306
- }
307
- }
308
- if (!server) {
309
- throw new Error("failed to start gateway server after retries");
310
- }
311
- const ws = new WebSocket(`ws://127.0.0.1:${port}`);
352
+ const started = await startGatewayServerWithRetries({ port, opts: gatewayOpts });
353
+ port = started.port;
354
+ const server = started.server;
355
+ const ws = new WebSocket(`ws://127.0.0.1:${port}`, wsHeaders ? { headers: wsHeaders } : undefined);
312
356
  await new Promise((resolve, reject) => {
313
357
  const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 10_000);
314
358
  const cleanup = () => {
@@ -349,20 +393,26 @@ export async function connectReq(ws, opts) {
349
393
  ? undefined
350
394
  : typeof testState.gatewayAuth?.token === "string"
351
395
  ? (testState.gatewayAuth.token ?? undefined)
352
- : (process.env.POOLBOT_GATEWAY_TOKEN ?? process.env.CLAWDBOT_GATEWAY_TOKEN);
396
+ : process.env.POOLBOT_GATEWAY_TOKEN;
353
397
  const defaultPassword = opts?.skipDefaultAuth === true
354
398
  ? undefined
355
399
  : typeof testState.gatewayAuth?.password === "string"
356
400
  ? (testState.gatewayAuth.password ?? undefined)
357
- : (process.env.POOLBOT_GATEWAY_PASSWORD ?? process.env.CLAWDBOT_GATEWAY_PASSWORD);
401
+ : process.env.POOLBOT_GATEWAY_PASSWORD;
358
402
  const token = opts?.token ?? defaultToken;
359
403
  const password = opts?.password ?? defaultPassword;
360
- const requestedScopes = Array.isArray(opts?.scopes) ? opts?.scopes : [];
404
+ const requestedScopes = Array.isArray(opts?.scopes)
405
+ ? opts.scopes
406
+ : role === "operator"
407
+ ? ["operator.admin"]
408
+ : [];
361
409
  const device = (() => {
362
- if (opts?.device === null)
410
+ if (opts?.device === null) {
363
411
  return undefined;
364
- if (opts?.device)
412
+ }
413
+ if (opts?.device) {
365
414
  return opts.device;
415
+ }
366
416
  const identity = loadOrCreateDeviceIdentity();
367
417
  const signedAtMs = Date.now();
368
418
  const payload = buildDeviceAuthPayload({
@@ -394,7 +444,7 @@ export async function connectReq(ws, opts) {
394
444
  commands: opts?.commands ?? [],
395
445
  permissions: opts?.permissions ?? undefined,
396
446
  role,
397
- scopes: opts?.scopes,
447
+ scopes: requestedScopes,
398
448
  auth: token || password
399
449
  ? {
400
450
  token,
@@ -405,8 +455,9 @@ export async function connectReq(ws, opts) {
405
455
  },
406
456
  }));
407
457
  const isResponseForId = (o) => {
408
- if (!o || typeof o !== "object" || Array.isArray(o))
458
+ if (!o || typeof o !== "object" || Array.isArray(o)) {
409
459
  return false;
460
+ }
410
461
  const rec = o;
411
462
  return rec.type === "res" && rec.id === id;
412
463
  };
@@ -418,13 +469,45 @@ export async function connectOk(ws, opts) {
418
469
  expect(res.payload?.type).toBe("hello-ok");
419
470
  return res.payload;
420
471
  }
472
+ export async function connectWebchatClient(params) {
473
+ const origin = params.origin ?? `http://127.0.0.1:${params.port}`;
474
+ const ws = new WebSocket(`ws://127.0.0.1:${params.port}`, {
475
+ headers: { origin },
476
+ });
477
+ await new Promise((resolve, reject) => {
478
+ const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 10_000);
479
+ const onOpen = () => {
480
+ clearTimeout(timer);
481
+ ws.off("error", onError);
482
+ resolve();
483
+ };
484
+ const onError = (err) => {
485
+ clearTimeout(timer);
486
+ ws.off("open", onOpen);
487
+ reject(err);
488
+ };
489
+ ws.once("open", onOpen);
490
+ ws.once("error", onError);
491
+ });
492
+ await connectOk(ws, {
493
+ client: params.client ??
494
+ {
495
+ id: GATEWAY_CLIENT_NAMES.WEBCHAT,
496
+ version: "1.0.0",
497
+ platform: "test",
498
+ mode: GATEWAY_CLIENT_MODES.WEBCHAT,
499
+ },
500
+ });
501
+ return ws;
502
+ }
421
503
  export async function rpcReq(ws, method, params, timeoutMs) {
422
504
  const { randomUUID } = await import("node:crypto");
423
505
  const id = randomUUID();
424
506
  ws.send(JSON.stringify({ type: "req", id, method, params }));
425
507
  return await onceMessage(ws, (o) => {
426
- if (!o || typeof o !== "object" || Array.isArray(o))
508
+ if (!o || typeof o !== "object" || Array.isArray(o)) {
427
509
  return false;
510
+ }
428
511
  const rec = o;
429
512
  return rec.type === "res" && rec.id === id;
430
513
  }, timeoutMs);
@@ -434,8 +517,9 @@ export async function waitForSystemEvent(timeoutMs = 2000) {
434
517
  const deadline = Date.now() + timeoutMs;
435
518
  while (Date.now() < deadline) {
436
519
  const events = peekSystemEvents(sessionKey);
437
- if (events.length > 0)
520
+ if (events.length > 0) {
438
521
  return events;
522
+ }
439
523
  await new Promise((resolve) => setTimeout(resolve, 10));
440
524
  }
441
525
  throw new Error("timeout waiting for system event");