@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
@@ -53,6 +53,57 @@ function resolveShellFromPath(name) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
+ function normalizeShellName(value) {
57
+ const trimmed = value.trim();
58
+ if (!trimmed) {
59
+ return "";
60
+ }
61
+ return path
62
+ .basename(trimmed)
63
+ .replace(/\.(exe|cmd|bat)$/i, "")
64
+ .replace(/[^a-zA-Z0-9_-]/g, "");
65
+ }
66
+ export function detectRuntimeShell() {
67
+ const overrideShell = process.env.CLAWDBOT_SHELL?.trim();
68
+ if (overrideShell) {
69
+ const name = normalizeShellName(overrideShell);
70
+ if (name) {
71
+ return name;
72
+ }
73
+ }
74
+ if (process.platform === "win32") {
75
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
76
+ return "pwsh";
77
+ }
78
+ return "powershell";
79
+ }
80
+ const envShell = process.env.SHELL?.trim();
81
+ if (envShell) {
82
+ const name = normalizeShellName(envShell);
83
+ if (name) {
84
+ return name;
85
+ }
86
+ }
87
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
88
+ return "pwsh";
89
+ }
90
+ if (process.env.BASH_VERSION) {
91
+ return "bash";
92
+ }
93
+ if (process.env.ZSH_VERSION) {
94
+ return "zsh";
95
+ }
96
+ if (process.env.FISH_VERSION) {
97
+ return "fish";
98
+ }
99
+ if (process.env.KSH_VERSION) {
100
+ return "ksh";
101
+ }
102
+ if (process.env.NU_VERSION || process.env.NUSHELL_VERSION) {
103
+ return "nu";
104
+ }
105
+ return undefined;
106
+ }
56
107
  export function sanitizeBinaryOutput(text) {
57
108
  const scrubbed = text.replace(/[\p{Format}\p{Surrogate}]/gu, "");
58
109
  if (!scrubbed)
@@ -0,0 +1,23 @@
1
+ import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
2
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
3
+ import { resolveBundledSkillsDir } from "./bundled-dir.js";
4
+ const skillsLogger = createSubsystemLogger("skills");
5
+ let hasWarnedMissingBundledDir = false;
6
+ export function resolveBundledSkillsContext(opts = {}) {
7
+ const dir = resolveBundledSkillsDir(opts);
8
+ const names = new Set();
9
+ if (!dir) {
10
+ if (!hasWarnedMissingBundledDir) {
11
+ hasWarnedMissingBundledDir = true;
12
+ skillsLogger.warn("Bundled skills directory could not be resolved; built-in skills may be missing.");
13
+ }
14
+ return { dir, names };
15
+ }
16
+ const result = loadSkillsFromDir({ dir, source: "poolbot-bundled" });
17
+ for (const skill of result.skills) {
18
+ if (skill.name.trim()) {
19
+ names.add(skill.name);
20
+ }
21
+ }
22
+ return { dir, names };
23
+ }
@@ -1,13 +1,37 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- export function resolveBundledSkillsDir() {
4
+ function looksLikeSkillsDir(dir) {
5
+ try {
6
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
7
+ for (const entry of entries) {
8
+ if (entry.name.startsWith(".")) {
9
+ continue;
10
+ }
11
+ const fullPath = path.join(dir, entry.name);
12
+ if (entry.isFile() && entry.name.endsWith(".md")) {
13
+ return true;
14
+ }
15
+ if (entry.isDirectory()) {
16
+ if (fs.existsSync(path.join(fullPath, "SKILL.md"))) {
17
+ return true;
18
+ }
19
+ }
20
+ }
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ return false;
26
+ }
27
+ export function resolveBundledSkillsDir(opts = {}) {
5
28
  const override = process.env.CLAWDBOT_BUNDLED_SKILLS_DIR?.trim();
6
29
  if (override)
7
30
  return override;
8
31
  // bun --compile: ship a sibling `skills/` next to the executable.
9
32
  try {
10
- const execDir = path.dirname(process.execPath);
33
+ const execPath = opts.execPath ?? process.execPath;
34
+ const execDir = path.dirname(execPath);
11
35
  const sibling = path.join(execDir, "skills");
12
36
  if (fs.existsSync(sibling))
13
37
  return sibling;
@@ -17,11 +41,21 @@ export function resolveBundledSkillsDir() {
17
41
  }
18
42
  // npm/dev: resolve `<packageRoot>/skills` relative to this module.
19
43
  try {
20
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
21
- const root = path.resolve(moduleDir, "..", "..", "..");
22
- const candidate = path.join(root, "skills");
23
- if (fs.existsSync(candidate))
24
- return candidate;
44
+ const moduleUrl = opts.moduleUrl ?? import.meta.url;
45
+ const moduleDir = path.dirname(fileURLToPath(moduleUrl));
46
+ // Walk up from this module to find the package root containing skills/
47
+ let current = moduleDir;
48
+ for (let depth = 0; depth < 6; depth += 1) {
49
+ const candidate = path.join(current, "skills");
50
+ if (looksLikeSkillsDir(candidate)) {
51
+ return candidate;
52
+ }
53
+ const next = path.dirname(current);
54
+ if (next === current) {
55
+ break;
56
+ }
57
+ current = next;
58
+ }
25
59
  }
26
60
  catch {
27
61
  // ignore
@@ -3,10 +3,45 @@ import path from "node:path";
3
3
  import { Readable } from "node:stream";
4
4
  import { pipeline } from "node:stream/promises";
5
5
  import { resolveBrewExecutable } from "../infra/brew.js";
6
+ import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
6
7
  import { runCommandWithTimeout } from "../process/exec.js";
8
+ import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
7
9
  import { CONFIG_DIR, ensureDir, resolveUserPath } from "../utils.js";
8
10
  import { hasBinary, loadWorkspaceSkillEntries, resolveSkillsInstallPreferences, } from "./skills.js";
9
11
  import { resolveSkillKey } from "./skills/frontmatter.js";
12
+ function withWarnings(result, warnings) {
13
+ if (warnings.length === 0)
14
+ return result;
15
+ return { ...result, warnings };
16
+ }
17
+ function formatScanFindingDetail(finding) {
18
+ const severity = finding.severity.toUpperCase();
19
+ const loc = finding.line ? `${finding.file}:${finding.line}` : finding.file;
20
+ return `[${severity}] ${loc} — ${finding.message}`;
21
+ }
22
+ async function collectSkillInstallScanWarnings(entry) {
23
+ const skillDir = entry.skill.filePath ? path.dirname(entry.skill.filePath) : undefined;
24
+ if (!skillDir)
25
+ return [];
26
+ try {
27
+ const summary = await scanDirectoryWithSummary(skillDir);
28
+ if (summary.findings.length === 0)
29
+ return [];
30
+ const warnings = [];
31
+ const header = summary.critical > 0
32
+ ? `⚠️ Security scan found ${summary.critical} critical finding(s).`
33
+ : `Security scan found ${summary.warn} warning(s).`;
34
+ warnings.push(header);
35
+ for (const finding of summary.findings) {
36
+ warnings.push(formatScanFindingDetail(finding));
37
+ }
38
+ warnings.push('Run "poolbot security audit --deep" for a full report.');
39
+ return warnings;
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
10
45
  function isNodeReadableStream(value) {
11
46
  return Boolean(value && typeof value.pipe === "function");
12
47
  }
@@ -110,10 +145,11 @@ function resolveArchiveType(spec, filename) {
110
145
  return undefined;
111
146
  }
112
147
  async function downloadFile(url, destPath, timeoutMs) {
113
- const controller = new AbortController();
114
- const timeout = setTimeout(() => controller.abort(), Math.max(1_000, timeoutMs));
148
+ const { response, release } = await fetchWithSsrFGuard({
149
+ url,
150
+ timeoutMs: Math.max(1_000, timeoutMs),
151
+ });
115
152
  try {
116
- const response = await fetch(url, { signal: controller.signal });
117
153
  if (!response.ok || !response.body) {
118
154
  throw new Error(`Download failed (${response.status} ${response.statusText})`);
119
155
  }
@@ -128,7 +164,7 @@ async function downloadFile(url, destPath, timeoutMs) {
128
164
  return { bytes: stat.size };
129
165
  }
130
166
  finally {
131
- clearTimeout(timeout);
167
+ await release();
132
168
  }
133
169
  }
134
170
  async function extractArchive(params) {
@@ -261,39 +297,40 @@ export async function installSkill(params) {
261
297
  code: null,
262
298
  };
263
299
  }
300
+ const warnings = await collectSkillInstallScanWarnings(entry);
264
301
  const spec = findInstallSpec(entry, params.installId);
265
302
  if (!spec) {
266
- return {
303
+ return withWarnings({
267
304
  ok: false,
268
305
  message: `Installer not found: ${params.installId}`,
269
306
  stdout: "",
270
307
  stderr: "",
271
308
  code: null,
272
- };
309
+ }, warnings);
273
310
  }
274
311
  if (spec.kind === "download") {
275
- return await installDownloadSpec({ entry, spec, timeoutMs });
312
+ return withWarnings(await installDownloadSpec({ entry, spec, timeoutMs }), warnings);
276
313
  }
277
314
  const prefs = resolveSkillsInstallPreferences(params.config);
278
315
  const command = buildInstallCommand(spec, prefs);
279
316
  if (command.error) {
280
- return {
317
+ return withWarnings({
281
318
  ok: false,
282
319
  message: command.error,
283
320
  stdout: "",
284
321
  stderr: "",
285
322
  code: null,
286
- };
323
+ }, warnings);
287
324
  }
288
325
  const brewExe = hasBinary("brew") ? "brew" : resolveBrewExecutable();
289
326
  if (spec.kind === "brew" && !brewExe) {
290
- return {
327
+ return withWarnings({
291
328
  ok: false,
292
329
  message: "brew not installed",
293
330
  stdout: "",
294
331
  stderr: "",
295
332
  code: null,
296
- };
333
+ }, warnings);
297
334
  }
298
335
  if (spec.kind === "uv" && !hasBinary("uv")) {
299
336
  if (brewExe) {
@@ -301,33 +338,33 @@ export async function installSkill(params) {
301
338
  timeoutMs,
302
339
  });
303
340
  if (brewResult.code !== 0) {
304
- return {
341
+ return withWarnings({
305
342
  ok: false,
306
343
  message: "Failed to install uv (brew)",
307
344
  stdout: brewResult.stdout.trim(),
308
345
  stderr: brewResult.stderr.trim(),
309
346
  code: brewResult.code,
310
- };
347
+ }, warnings);
311
348
  }
312
349
  }
313
350
  else {
314
- return {
351
+ return withWarnings({
315
352
  ok: false,
316
353
  message: "uv not installed (install via brew)",
317
354
  stdout: "",
318
355
  stderr: "",
319
356
  code: null,
320
- };
357
+ }, warnings);
321
358
  }
322
359
  }
323
360
  if (!command.argv || command.argv.length === 0) {
324
- return {
361
+ return withWarnings({
325
362
  ok: false,
326
363
  message: "invalid install command",
327
364
  stdout: "",
328
365
  stderr: "",
329
366
  code: null,
330
- };
367
+ }, warnings);
331
368
  }
332
369
  if (spec.kind === "brew" && brewExe && command.argv[0] === "brew") {
333
370
  command.argv[0] = brewExe;
@@ -338,23 +375,23 @@ export async function installSkill(params) {
338
375
  timeoutMs,
339
376
  });
340
377
  if (brewResult.code !== 0) {
341
- return {
378
+ return withWarnings({
342
379
  ok: false,
343
380
  message: "Failed to install go (brew)",
344
381
  stdout: brewResult.stdout.trim(),
345
382
  stderr: brewResult.stderr.trim(),
346
383
  code: brewResult.code,
347
- };
384
+ }, warnings);
348
385
  }
349
386
  }
350
387
  else {
351
- return {
388
+ return withWarnings({
352
389
  ok: false,
353
390
  message: "go not installed (install via brew)",
354
391
  stdout: "",
355
392
  stderr: "",
356
393
  code: null,
357
- };
394
+ }, warnings);
358
395
  }
359
396
  }
360
397
  let env;
@@ -380,11 +417,11 @@ export async function installSkill(params) {
380
417
  }
381
418
  })();
382
419
  const success = result.code === 0;
383
- return {
420
+ return withWarnings({
384
421
  ok: success,
385
422
  message: success ? "Installed" : formatInstallFailureMessage(result),
386
423
  stdout: result.stdout.trim(),
387
424
  stderr: result.stderr.trim(),
388
425
  code: result.code,
389
- };
426
+ }, warnings);
390
427
  }
@@ -1,28 +1,16 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
+ import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
3
4
  import { loadConfig } from "../config/config.js";
4
5
  import { loadSessionStore, resolveAgentIdFromSessionKey, resolveMainSessionKey, resolveStorePath, } from "../config/sessions.js";
5
- import { normalizeMainKey } from "../routing/session-key.js";
6
- import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
7
6
  import { callGateway } from "../gateway/call.js";
7
+ import { formatDurationCompact } from "../infra/format-time/format-duration.js";
8
+ import { normalizeMainKey } from "../routing/session-key.js";
8
9
  import { defaultRuntime } from "../runtime.js";
9
10
  import { deliveryContextFromSession, mergeDeliveryContext, normalizeDeliveryContext, } from "../utils/delivery-context.js";
10
- import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
11
+ import { isEmbeddedPiRunActive, queueEmbeddedPiMessage, waitForEmbeddedPiRunEnd, } from "./pi-embedded.js";
11
12
  import { enqueueAnnounce } from "./subagent-announce-queue.js";
12
13
  import { readLatestAssistantReply } from "./tools/agent-step.js";
13
- function formatDurationShort(valueMs) {
14
- if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0)
15
- return undefined;
16
- const totalSeconds = Math.round(valueMs / 1000);
17
- const hours = Math.floor(totalSeconds / 3600);
18
- const minutes = Math.floor((totalSeconds % 3600) / 60);
19
- const seconds = totalSeconds % 60;
20
- if (hours > 0)
21
- return `${hours}h${minutes}m`;
22
- if (minutes > 0)
23
- return `${minutes}m${seconds}s`;
24
- return `${seconds}s`;
25
- }
26
14
  function formatTokenCount(value) {
27
15
  if (!value || !Number.isFinite(value))
28
16
  return "0";
@@ -72,7 +60,10 @@ async function waitForSessionUsage(params) {
72
60
  return { entry, storePath };
73
61
  }
74
62
  function resolveAnnounceOrigin(entry, requesterOrigin) {
75
- return mergeDeliveryContext(deliveryContextFromSession(entry), requesterOrigin);
63
+ // requesterOrigin (captured at spawn time) reflects the channel the user is
64
+ // actually on and must take priority over the session entry, which may carry
65
+ // stale lastChannel / lastTo values from a previous channel interaction.
66
+ return mergeDeliveryContext(requesterOrigin, deliveryContextFromSession(entry));
76
67
  }
77
68
  async function sendAnnounce(item) {
78
69
  const origin = item.origin;
@@ -178,7 +169,7 @@ async function buildSubagentStatsLine(params) {
178
169
  ? (input * costConfig.input + output * costConfig.output) / 1_000_000
179
170
  : undefined;
180
171
  const parts = [];
181
- const runtime = formatDurationShort(runtimeMs);
172
+ const runtime = formatDurationCompact(runtimeMs);
182
173
  parts.push(`runtime ${runtime ?? "n/a"}`);
183
174
  if (typeof total === "number") {
184
175
  const inputText = typeof input === "number" ? formatTokenCount(input) : "n/a";
@@ -199,6 +190,28 @@ async function buildSubagentStatsLine(params) {
199
190
  parts.push(`transcript ${transcriptPath}`);
200
191
  return `Stats: ${parts.join(" \u2022 ")}`;
201
192
  }
193
+ function loadSessionEntryByKey(sessionKey) {
194
+ const cfg = loadConfig();
195
+ const agentId = resolveAgentIdFromSessionKey(sessionKey);
196
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
197
+ const store = loadSessionStore(storePath);
198
+ return store[sessionKey];
199
+ }
200
+ async function readLatestAssistantReplyWithRetry(params) {
201
+ let reply = params.initialReply?.trim() ? params.initialReply : undefined;
202
+ if (reply) {
203
+ return reply;
204
+ }
205
+ const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
206
+ while (Date.now() < deadline) {
207
+ await new Promise((resolve) => setTimeout(resolve, 300));
208
+ const latest = await readLatestAssistantReply({ sessionKey: params.sessionKey });
209
+ if (latest?.trim()) {
210
+ return latest;
211
+ }
212
+ }
213
+ return reply;
214
+ }
202
215
  export function buildSubagentSystemPrompt(params) {
203
216
  const taskText = typeof params.task === "string" && params.task.trim()
204
217
  ? params.task.replace(/\s+/g, " ").trim()
@@ -227,10 +240,10 @@ export function buildSubagentSystemPrompt(params) {
227
240
  "",
228
241
  "## What You DON'T Do",
229
242
  "- NO user conversations (that's main agent's job)",
230
- "- NO external messages (email, tweets, etc.) unless explicitly tasked",
243
+ "- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel",
231
244
  "- NO cron jobs or persistent state",
232
245
  "- NO pretending to be the main agent",
233
- "- NO using the `message` tool directly",
246
+ "- Only use the `message` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the main agent deliver it",
234
247
  "",
235
248
  "## Session Context",
236
249
  params.label ? `- Label: ${params.label}` : undefined,
@@ -245,25 +258,46 @@ export function buildSubagentSystemPrompt(params) {
245
258
  }
246
259
  export async function runSubagentAnnounceFlow(params) {
247
260
  let didAnnounce = false;
261
+ let shouldDeleteChildSession = params.cleanup === "delete";
248
262
  try {
249
263
  const requesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
264
+ const childSessionId = (() => {
265
+ const entry = loadSessionEntryByKey(params.childSessionKey);
266
+ return typeof entry?.sessionId === "string" && entry.sessionId.trim()
267
+ ? entry.sessionId.trim()
268
+ : undefined;
269
+ })();
270
+ const settleTimeoutMs = Math.min(Math.max(params.timeoutMs, 1), 120_000);
250
271
  let reply = params.roundOneReply;
251
272
  let outcome = params.outcome;
273
+ // Lifecycle "end" can arrive before auto-compaction retries finish. If the
274
+ // subagent is still active, wait for the embedded run to fully settle.
275
+ if (childSessionId && isEmbeddedPiRunActive(childSessionId)) {
276
+ const settled = await waitForEmbeddedPiRunEnd(childSessionId, settleTimeoutMs);
277
+ if (!settled && isEmbeddedPiRunActive(childSessionId)) {
278
+ // The child run is still active (e.g., compaction retry still in progress).
279
+ // Defer announcement so we don't report stale/partial output.
280
+ // Keep the child session so output is not lost while the run is still active.
281
+ shouldDeleteChildSession = false;
282
+ return false;
283
+ }
284
+ }
252
285
  if (!reply && params.waitForCompletion !== false) {
253
- const waitMs = Math.min(params.timeoutMs, 60_000);
254
- const wait = (await callGateway({
286
+ const waitMs = settleTimeoutMs;
287
+ const wait = await callGateway({
255
288
  method: "agent.wait",
256
289
  params: {
257
290
  runId: params.childRunId,
258
291
  timeoutMs: waitMs,
259
292
  },
260
293
  timeoutMs: waitMs + 2000,
261
- }));
294
+ });
295
+ const waitError = typeof wait?.error === "string" ? wait.error : undefined;
262
296
  if (wait?.status === "timeout") {
263
297
  outcome = { status: "timeout" };
264
298
  }
265
299
  else if (wait?.status === "error") {
266
- outcome = { status: "error", error: wait.error };
300
+ outcome = { status: "error", error: waitError };
267
301
  }
268
302
  else if (wait?.status === "ok") {
269
303
  outcome = { status: "ok" };
@@ -275,20 +309,30 @@ export async function runSubagentAnnounceFlow(params) {
275
309
  params.endedAt = wait.endedAt;
276
310
  }
277
311
  if (wait?.status === "timeout") {
278
- if (!outcome)
312
+ if (!outcome) {
279
313
  outcome = { status: "timeout" };
314
+ }
280
315
  }
281
- reply = await readLatestAssistantReply({
282
- sessionKey: params.childSessionKey,
283
- });
316
+ reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
284
317
  }
285
318
  if (!reply) {
286
- reply = await readLatestAssistantReply({
319
+ reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
320
+ }
321
+ if (!reply?.trim()) {
322
+ reply = await readLatestAssistantReplyWithRetry({
287
323
  sessionKey: params.childSessionKey,
324
+ initialReply: reply,
325
+ maxWaitMs: params.timeoutMs,
288
326
  });
289
327
  }
290
- if (!outcome)
328
+ if (!reply?.trim() && childSessionId && isEmbeddedPiRunActive(childSessionId)) {
329
+ // Avoid announcing "(no output)" while the child run is still producing output.
330
+ shouldDeleteChildSession = false;
331
+ return false;
332
+ }
333
+ if (!outcome) {
291
334
  outcome = { status: "unknown" };
335
+ }
292
336
  // Build stats
293
337
  const statsLine = await buildSubagentStatsLine({
294
338
  sessionKey: params.childSessionKey,
@@ -304,9 +348,10 @@ export async function runSubagentAnnounceFlow(params) {
304
348
  ? `failed: ${outcome.error || "unknown error"}`
305
349
  : "finished with unknown status";
306
350
  // Build instructional message for main agent
307
- const taskLabel = params.label || params.task || "background task";
351
+ const announceType = params.announceType ?? "subagent task";
352
+ const taskLabel = params.label || params.task || "task";
308
353
  const triggerMessage = [
309
- `A background task "${taskLabel}" just ${statusLabel}.`,
354
+ `A ${announceType} "${taskLabel}" just ${statusLabel}.`,
310
355
  "",
311
356
  "Findings:",
312
357
  reply || "(no output)",
@@ -314,7 +359,7 @@ export async function runSubagentAnnounceFlow(params) {
314
359
  statsLine,
315
360
  "",
316
361
  "Summarize this naturally for the user. Keep it brief (1-2 sentences). Flow it into the conversation naturally.",
317
- "Do not mention technical details like tokens, stats, or that this was a background task.",
362
+ `Do not mention technical details like tokens, stats, or that this was a ${announceType}.`,
318
363
  "You can respond with NO_REPLY if no announcement is needed (e.g., internal task with no user-facing result).",
319
364
  ].join("\n");
320
365
  const queued = await maybeQueueSubagentAnnounce({
@@ -374,7 +419,7 @@ export async function runSubagentAnnounceFlow(params) {
374
419
  // Best-effort
375
420
  }
376
421
  }
377
- if (params.cleanup === "delete") {
422
+ if (shouldDeleteChildSession) {
378
423
  try {
379
424
  await callGateway({
380
425
  method: "sessions.delete",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Conformance snapshot for tool policy.
3
+ *
4
+ * Security note:
5
+ * - This is static, build-time information (no runtime I/O, no network exposure).
6
+ * - Intended for CI/tools to detect drift between the implementation policy and
7
+ * the formal models/extractors.
8
+ */
9
+ import { TOOL_GROUPS } from "./tool-policy.js";
10
+ // Tool name aliases are intentionally not exported from tool-policy today.
11
+ // Keep the conformance snapshot focused on exported policy constants.
12
+ export const TOOL_POLICY_CONFORMANCE = {
13
+ toolGroups: TOOL_GROUPS,
14
+ };
@@ -47,6 +47,7 @@ export const TOOL_GROUPS = {
47
47
  "image",
48
48
  ],
49
49
  };
50
+ const OWNER_ONLY_TOOL_NAMES = new Set(["whatsapp_login"]);
50
51
  const TOOL_PROFILES = {
51
52
  minimal: {
52
53
  allow: ["session_status"],
@@ -69,6 +70,29 @@ export function normalizeToolName(name) {
69
70
  const normalized = name.trim().toLowerCase();
70
71
  return TOOL_NAME_ALIASES[normalized] ?? normalized;
71
72
  }
73
+ export function isOwnerOnlyToolName(name) {
74
+ return OWNER_ONLY_TOOL_NAMES.has(normalizeToolName(name));
75
+ }
76
+ export function applyOwnerOnlyToolPolicy(tools, senderIsOwner) {
77
+ const withGuard = tools.map((tool) => {
78
+ if (!isOwnerOnlyToolName(tool.name)) {
79
+ return tool;
80
+ }
81
+ if (senderIsOwner || !tool.execute) {
82
+ return tool;
83
+ }
84
+ return {
85
+ ...tool,
86
+ execute: async () => {
87
+ throw new Error("Tool restricted to owner senders.");
88
+ },
89
+ };
90
+ });
91
+ if (senderIsOwner) {
92
+ return withGuard;
93
+ }
94
+ return withGuard.filter((tool) => !isOwnerOnlyToolName(tool.name));
95
+ }
72
96
  export function normalizeToolList(list) {
73
97
  if (!list)
74
98
  return [];