negotium 0.1.6 → 0.1.8

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 (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,865 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { canonicalMcpBridgeEnv } from "#mcp/canonical-bridge-config";
3
+ import { buildRuntimeMcpSpec, RUNTIME_MCP_KEY } from "#mcp/runtime-spec";
4
+ import { peerSessionBridgeIpcEnv } from "#mcp/session-comm/bridge-ipc-config";
5
+ import {
6
+ AGENT_HEALTH_SERVER,
7
+ CANONICAL_MCP_PROXY_SERVER,
8
+ FALLBACK_AGENT,
9
+ PADDLEOCR_SERVER,
10
+ resolveTopicWorkspaceDir,
11
+ SESSION_COMM_SERVER,
12
+ SYSTEM_HEALTH_SERVER,
13
+ TASK_SERVER,
14
+ TOKEN_STATS_SERVER,
15
+ TSCONFIG_PATH,
16
+ TSX_BIN,
17
+ VAULT_SERVER,
18
+ WIKI_SERVER,
19
+ } from "#platform/config";
20
+ import { logger } from "#platform/logger";
21
+ import type { AgentKind, AgentQueryOptions, PeerRuntimeBridgeContext } from "#types";
22
+
23
+ /**
24
+ * Build the stdio transport spec for a Otium MCP server entry.
25
+ *
26
+ * claude/maestro launch the .ts server directly with `bun run` (fast, native
27
+ * TypeScript + bun:sqlite). codex CANNOT — codex 0.135's rmcp stdio client
28
+ * fails to handshake with bun-spawned servers (initialize is dropped/raced, so
29
+ * no tools reach the model), while pure-node servers connect reliably. So for
30
+ * the `codex` agent we launch the SAME server file via node + tsx, passing the
31
+ * tsconfig explicitly (servers run with cwd ≠ PROJECT_ROOT, so tsx can't find
32
+ * it on its own) so `@/*` path aliases resolve. Per-turn agent-aware config
33
+ * means switching agents mid-topic just works: each turn re-spawns with the
34
+ * right runtime.
35
+ */
36
+ export function buildStdioMcpServer(
37
+ agent: AgentKind | undefined,
38
+ serverFile: string,
39
+ serverArgs: string[],
40
+ env?: Record<string, string>,
41
+ ): Record<string, unknown> {
42
+ if (agent === "codex") {
43
+ return {
44
+ command: "node",
45
+ args: [TSX_BIN, serverFile, ...serverArgs],
46
+ // Merged onto codex's inherited env (not a replacement) — verified.
47
+ env: { TSX_TSCONFIG_PATH: TSCONFIG_PATH, ...env },
48
+ };
49
+ }
50
+ return {
51
+ command: "bun",
52
+ args: ["run", serverFile, ...serverArgs],
53
+ ...(env ? { env } : {}),
54
+ };
55
+ }
56
+
57
+ // --- Playwright unavailable notifier + per-turn state ---
58
+ //
59
+ // When the playwright manager fails to allocate a long-lived MCP port,
60
+ // the catalog used to fall back to a per-turn stdio chromium that ran as
61
+ // a child of the agent CLI subprocess — which meant Chromium was bound
62
+ // to the agent's process tree and died on cascade-kill. We now skip the
63
+ // browser entry entirely (the playwright entry is omitted from the
64
+ // catalog for that turn) AND surface the gap two ways:
65
+ //
66
+ // 1. `notifyPlaywrightUnavailable` — fire-and-forget callback for the host
67
+ // (telegram bot) to push an alert to the current topic. Throttled
68
+ // per (userId, topic) with a 5-minute cooldown so a flaky
69
+ // manager doesn't spam the chat. Per-process state — restart clears.
70
+ //
71
+ // 2. `consumePlaywrightUnavailable` — the system-prompt builder reads
72
+ // this flag to add an explicit "Playwright unavailable this turn"
73
+ // reminder to the agent's user message, so the model knows to
74
+ // route around browser tools instead of hallucinating a call to a
75
+ // tool that was silently removed from the schema list.
76
+
77
+ type PlaywrightUnavailableContext = {
78
+ userId: string;
79
+ topic: string | undefined;
80
+ agent: AgentKind | undefined;
81
+ };
82
+
83
+ type PlaywrightUnavailableNotifier = (ctx: PlaywrightUnavailableContext) => void;
84
+
85
+ let _playwrightUnavailableNotifier: PlaywrightUnavailableNotifier | undefined;
86
+ const _playwrightUnavailableLastNotifiedAt = new Map<string, number>();
87
+ const _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS = 5 * 60_000;
88
+
89
+ const _playwrightUnavailableThisTurn = new Set<string>();
90
+
91
+ function _playwrightUnavailableKey(userId: string, topic: string | undefined): string {
92
+ return `${userId}:${topic ?? ""}`;
93
+ }
94
+
95
+ /**
96
+ * Register the host's "playwright unavailable" alert sink. Called once at bot
97
+ * startup. Idempotent — last writer wins.
98
+ */
99
+ export function setPlaywrightUnavailableNotifier(cb: PlaywrightUnavailableNotifier): void {
100
+ _playwrightUnavailableNotifier = cb;
101
+ }
102
+
103
+ /**
104
+ * Mark the current turn as missing long-lived Playwright and notify the host.
105
+ * The playwright catalog entry calls this when it is built without a port, but
106
+ * route-level callers can call it earlier after ensurePlaywright fails so the
107
+ * system prompt for the same turn can include the unavailable reminder.
108
+ */
109
+ export function markPlaywrightUnavailable(ctx: PlaywrightUnavailableContext): void {
110
+ _markPlaywrightUnavailable(ctx);
111
+ }
112
+
113
+ function _markPlaywrightUnavailable(ctx: PlaywrightUnavailableContext): void {
114
+ // Mark unavailable for the next system-prompt build, regardless of
115
+ // notifier presence (the model-side reminder must always fire so the
116
+ // agent doesn't try to call a missing tool).
117
+ _playwrightUnavailableThisTurn.add(_playwrightUnavailableKey(ctx.userId, ctx.topic));
118
+
119
+ if (!_playwrightUnavailableNotifier) return;
120
+ const key = _playwrightUnavailableKey(ctx.userId, ctx.topic);
121
+ const now = Date.now();
122
+ const last = _playwrightUnavailableLastNotifiedAt.get(key) ?? 0;
123
+ if (now - last < _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS) return;
124
+ _playwrightUnavailableLastNotifiedAt.set(key, now);
125
+ try {
126
+ _playwrightUnavailableNotifier(ctx);
127
+ } catch (err) {
128
+ logger.warn({ err }, "playwright unavailable notifier threw");
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Read + clear the "playwright unavailable this turn" flag for a
134
+ * (user, group, topic). Used by the system-prompt builder right before
135
+ * the agent query starts. Single consumer pattern — calling more than
136
+ * once per turn returns `false` for the second caller (intended; the
137
+ * reminder only needs to ride one prompt).
138
+ */
139
+ export function consumePlaywrightUnavailable(userId: string, topic: string | undefined): boolean {
140
+ const key = _playwrightUnavailableKey(userId, topic);
141
+ if (!_playwrightUnavailableThisTurn.has(key)) return false;
142
+ _playwrightUnavailableThisTurn.delete(key);
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Single source of truth for MCP server registration.
148
+ *
149
+ * Each entry declares which scopes it participates in (`dm`, `forum`, `fork`, `cron`)
150
+ * and how to build its spawn config given a context. Public helpers
151
+ * (`getDmMcpServers`, `getForumMcpServers`, `getForkMcpServers`) and the
152
+ * derived `ALL_FORUM_MCP_SERVER_NAMES` / `REQUIRED_FORUM_MCP_SERVERS`
153
+ * constants all read from this catalog, so adding a new MCP server is a
154
+ * one-line append here instead of touching three places.
155
+ *
156
+ * Conventions:
157
+ * - `scopes` lists every scope the server should appear in by default.
158
+ * - `forumRequired: true` keeps the server even when a forum topic
159
+ * restricts the enabled list via `configure_mcp` (forum scope only).
160
+ * - `build(ctx)` returns the raw transport config (Claude-SDK shape).
161
+ * Codex callers funnel through `toCodexMcpServers` separately.
162
+ *
163
+ * Insertion order is preserved by `Object.entries`, which determines the
164
+ * order of server names in user-facing displays.
165
+ */
166
+
167
+ export type RuntimeMcpScope = "dm" | "forum" | "fork" | "manager" | "cron";
168
+
169
+ export interface RuntimeMcpBuildContext {
170
+ userId: string;
171
+ /** "dm" for DM scope, topic/session name for forum/fork. */
172
+ session: string;
173
+ /** REST/WS topic id when known. Prefer for authorization/scope checks. */
174
+ topicId?: string;
175
+ /** REST/WS query id when known. */
176
+ queryId?: string;
177
+ /** Topic id whose wiki memory should be read/written. */
178
+ wikiTopicId?: string;
179
+ agent?: AgentKind;
180
+ cwd?: string;
181
+ model?: string;
182
+ currentUserPrompt?: string;
183
+ depth?: number;
184
+ playwrightPort?: number;
185
+ playwrightCapability?: string;
186
+ bgBashPort?: number;
187
+ autoContinue?: boolean;
188
+ silent?: boolean;
189
+ peerBridge?: PeerRuntimeBridgeContext;
190
+ }
191
+
192
+ export interface RuntimeMcpCatalogEntry {
193
+ scopes: readonly RuntimeMcpScope[];
194
+ /** Forum scope only: cannot be removed via the enabled whitelist. */
195
+ forumRequired?: boolean;
196
+ /**
197
+ * Returns the raw transport spec (Claude-SDK shape), or `null` to
198
+ * signal "this server is unavailable for this turn — omit it from the
199
+ * catalog entirely." Used by the playwright entry when the manager
200
+ * cannot provide a long-lived port.
201
+ */
202
+ build(ctx: RuntimeMcpBuildContext): Record<string, unknown> | null;
203
+ }
204
+
205
+ // --- Playwright transport builders ---
206
+
207
+ export const CODEX_BROWSER_CAPABILITY_ENV = "NEGOTIUM_BROWSER_CAPABILITY";
208
+
209
+ export function browserOwnerCapability(capability: string, owner: string): string {
210
+ return createHmac("sha256", capability).update(owner).digest("hex");
211
+ }
212
+
213
+ export function browserOwnerForContext(ctx: {
214
+ userId?: string;
215
+ session?: string;
216
+ topicId?: string;
217
+ }): string | undefined {
218
+ if (ctx.topicId) return `topic:${ctx.topicId}`;
219
+ if (ctx.userId && ctx.session) return `user:${ctx.userId}:${ctx.session}`;
220
+ return undefined;
221
+ }
222
+
223
+ function playwrightTransport(port: number, owner: string, capability: string, agent?: AgentKind) {
224
+ const ownerCapability = browserOwnerCapability(capability, owner);
225
+ if (agent === "codex") {
226
+ return {
227
+ url: `http://127.0.0.1:${port}/mcp`,
228
+ http_headers: { "X-Browser-Owner": owner },
229
+ env_http_headers: { "X-Browser-Capability": CODEX_BROWSER_CAPABILITY_ENV },
230
+ };
231
+ }
232
+ if (agent === "maestro") {
233
+ const query = new URLSearchParams({ owner, capability: ownerCapability });
234
+ return {
235
+ type: "sse" as const,
236
+ url: `http://127.0.0.1:${port}/sse?${query}`,
237
+ };
238
+ }
239
+ return {
240
+ type: "sse" as const,
241
+ url: `http://127.0.0.1:${port}/sse`,
242
+ headers: { "X-Browser-Owner": owner, "X-Browser-Capability": ownerCapability },
243
+ };
244
+ }
245
+
246
+ function longLivedHttpMcp(agent: AgentKind | undefined, port: number) {
247
+ return agent === "codex"
248
+ ? { url: `http://127.0.0.1:${port}/mcp` }
249
+ : { type: "sse" as const, url: `http://127.0.0.1:${port}/sse` };
250
+ }
251
+
252
+ // --- Catalog ---
253
+
254
+ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
255
+ playwright: {
256
+ scopes: ["dm", "forum", "fork", "cron"],
257
+ build({ userId, session, topicId, playwrightPort, playwrightCapability, agent }) {
258
+ // Codex uses streamable HTTP while Claude and Maestro use SSE. Both
259
+ // transports terminate at the same long-lived browser/profile server.
260
+ if (playwrightPort && playwrightCapability) {
261
+ const owner = browserOwnerForContext({ userId, session, topicId });
262
+ if (!owner) return null;
263
+ return playwrightTransport(playwrightPort, owner, playwrightCapability, agent);
264
+ }
265
+ // No port available — the playwright manager could not allocate one
266
+ // for this turn. The previous behavior was to fall back to a per-turn
267
+ // stdio chromium, but that made Chromium a child of the agent CLI
268
+ // process tree, so cascade-kill on agent exit took the browser down
269
+ // with it (visible in pm2 as "Chromium died with claude"). We now
270
+ // omit the playwright entry from this turn's catalog AND fire two
271
+ // side-effects:
272
+ // 1. notify the host so it can push an alert into the current
273
+ // topic (rate-limited 5min per scope key);
274
+ // 2. mark the (user, topic) as "playwright unavailable
275
+ // this turn" so the system-prompt builder can tell the model
276
+ // explicitly, instead of letting the model hallucinate a call
277
+ // to a tool whose schema silently vanished.
278
+ _markPlaywrightUnavailable({
279
+ userId,
280
+ topic: session,
281
+ agent,
282
+ });
283
+ return null;
284
+ },
285
+ },
286
+ [RUNTIME_MCP_KEY]: {
287
+ scopes: ["forum", "manager", "fork", "cron"],
288
+ forumRequired: true,
289
+ build({
290
+ userId,
291
+ session,
292
+ topicId,
293
+ queryId,
294
+ agent,
295
+ cwd,
296
+ model,
297
+ currentUserPrompt,
298
+ autoContinue,
299
+ peerBridge,
300
+ }) {
301
+ if (!topicId || !agent) return null;
302
+ return buildRuntimeMcpSpec(agent, {
303
+ userId,
304
+ topicId,
305
+ topicTitle: session,
306
+ queryId,
307
+ cwd: cwd ?? resolveTopicWorkspaceDir(topicId),
308
+ agent,
309
+ model,
310
+ currentUserPrompt,
311
+ autoContinue,
312
+ peerBridge,
313
+ });
314
+ },
315
+ },
316
+ paddleocr: {
317
+ scopes: ["dm", "forum", "fork", "cron"],
318
+ build({ agent }) {
319
+ return buildStdioMcpServer(agent, PADDLEOCR_SERVER, []);
320
+ },
321
+ },
322
+ "token-stats": {
323
+ scopes: ["dm", "forum", "manager", "cron"],
324
+ forumRequired: true,
325
+ build({ userId, agent }) {
326
+ return buildStdioMcpServer(agent, TOKEN_STATS_SERVER, [`--user-id=${userId}`]);
327
+ },
328
+ },
329
+ // Otium-owned shared task system. This is the only authoritative task/todo
330
+ // surface across claude/codex/maestro; provider-native task stores are
331
+ // blocked or ignored because they do not survive agent switches.
332
+ task: {
333
+ scopes: ["dm", "forum", "manager", "cron"],
334
+ forumRequired: true,
335
+ build({ userId, session, topicId, queryId, agent, peerBridge }) {
336
+ if (peerBridge) {
337
+ if (!topicId || !queryId) return null;
338
+ const env = canonicalMcpBridgeEnv({
339
+ surface: "task",
340
+ userId,
341
+ topicId,
342
+ queryId,
343
+ peerBridge,
344
+ });
345
+ return env
346
+ ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=task"], env)
347
+ : null;
348
+ }
349
+ const args = [`--user-id=${userId}`, `--topic=${session}`];
350
+ if (topicId) args.push(`--topic-id=${topicId}`);
351
+ return buildStdioMcpServer(agent, TASK_SERVER, args);
352
+ },
353
+ },
354
+ "session-comm": {
355
+ // Exposed in `manager` scope as well so the General-topic manager agent
356
+ // can wake fresh-created topics via tell_session/ask_session right after
357
+ // create_topic. Without this, a newly-created topic would stay in the
358
+ // "fresh-start ready" state until the user manually visits it.
359
+ scopes: ["forum", "fork", "manager"],
360
+ forumRequired: true,
361
+ build({ userId, session, topicId, agent, depth = 0, silent, peerBridge }) {
362
+ const effectiveAgent = agent ?? FALLBACK_AGENT;
363
+ const args = [
364
+ `--user-id=${userId}`,
365
+ `--topic=${session}`,
366
+ ...(topicId ? [`--topic-id=${topicId}`] : []),
367
+ `--depth=${depth}`,
368
+ `--agent=${effectiveAgent}`,
369
+ ...(silent ? ["--reply-only=true"] : []),
370
+ ...(peerBridge ? [`--peer-host-query-id=${peerBridge.hostQueryId}`] : []),
371
+ ];
372
+ return buildStdioMcpServer(
373
+ effectiveAgent,
374
+ SESSION_COMM_SERVER,
375
+ args,
376
+ peerBridge ? peerSessionBridgeIpcEnv() : undefined,
377
+ );
378
+ },
379
+ },
380
+ wiki: {
381
+ scopes: ["dm", "forum", "manager", "cron"],
382
+ forumRequired: true,
383
+ build({ userId, session, topicId, queryId, wikiTopicId, agent, peerBridge }) {
384
+ if (peerBridge) {
385
+ if (!topicId || !queryId) return null;
386
+ const env = canonicalMcpBridgeEnv({
387
+ surface: "wiki",
388
+ userId,
389
+ topicId,
390
+ queryId,
391
+ peerBridge,
392
+ });
393
+ return env
394
+ ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env)
395
+ : null;
396
+ }
397
+ const args = [`--user-id=${userId}`];
398
+ // Prefer the REST topic id so save_wiki_entry writes filenames and SQLite
399
+ // briefs keyed the same way the topic wiki API reads them.
400
+ const resolvedWikiTopicId =
401
+ wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
402
+ if (resolvedWikiTopicId) args.push(`--topic-id=${resolvedWikiTopicId}`);
403
+ args.push("--surface=wiki");
404
+ return buildStdioMcpServer(agent, WIKI_SERVER, args);
405
+ },
406
+ },
407
+ skills: {
408
+ scopes: ["dm", "forum", "manager", "cron"],
409
+ forumRequired: true,
410
+ build({ userId, topicId, agent }) {
411
+ const args = [`--user-id=${userId}`, "--surface=skills"];
412
+ if (topicId) args.push(`--topic-id=${topicId}`);
413
+ return buildStdioMcpServer(agent, WIKI_SERVER, args);
414
+ },
415
+ },
416
+ "system-health": {
417
+ scopes: ["dm", "forum", "manager", "cron"],
418
+ forumRequired: true,
419
+ build({ agent }) {
420
+ return buildStdioMcpServer(agent, SYSTEM_HEALTH_SERVER, []);
421
+ },
422
+ },
423
+ "background-bash": {
424
+ scopes: ["forum"],
425
+ build({ agent, bgBashPort }) {
426
+ if (bgBashPort === undefined) return null;
427
+ return longLivedHttpMcp(agent, bgBashPort);
428
+ },
429
+ },
430
+ "agent-health": {
431
+ scopes: ["forum", "manager", "cron"],
432
+ forumRequired: true,
433
+ build({ userId, agent }) {
434
+ const args = [`--user-id=${userId}`];
435
+ return buildStdioMcpServer(agent, AGENT_HEALTH_SERVER, args);
436
+ },
437
+ },
438
+ vault: {
439
+ scopes: ["dm", "forum", "manager", "cron"],
440
+ // Credential references must never dead-end because a topic whitelist
441
+ // omitted the broker while provider hooks correctly block raw expansion.
442
+ forumRequired: true,
443
+ build({ userId, agent }) {
444
+ const args = [`--user-id=${userId}`];
445
+ // Codex has no host-side PostToolUse redaction. Keep its Vault surface
446
+ // broker-only and non-persistent: HTTPS responses are scrubbed before the
447
+ // MCP result reaches Codex, while arbitrary shell could write a secret to
448
+ // disk and reveal it through a later Read.
449
+ if (agent === "codex") args.push("--http-only=true");
450
+ return buildStdioMcpServer(agent, VAULT_SERVER, args);
451
+ },
452
+ },
453
+ };
454
+
455
+ // --- Derived catalog views ---
456
+
457
+ function namesInScope(scope: RuntimeMcpScope): string[] {
458
+ return Object.entries(MCP_CATALOG)
459
+ .filter(([, e]) => e.scopes.includes(scope))
460
+ .map(([name]) => name);
461
+ }
462
+
463
+ /** All forum-eligible MCP server names, in display order. */
464
+ const allForumMcpServerNames: string[] = [];
465
+ export const ALL_FORUM_MCP_SERVER_NAMES: readonly string[] = allForumMcpServerNames;
466
+
467
+ /** Forum servers that cannot be removed via the enabled whitelist. */
468
+ const requiredForumMcpServers: string[] = [];
469
+ export const REQUIRED_FORUM_MCP_SERVERS: readonly string[] = requiredForumMcpServers;
470
+
471
+ /** Forum servers that can be toggled via the enabled whitelist (not required). */
472
+ const optionalForumMcpServers: string[] = [];
473
+ export const OPTIONAL_FORUM_MCP_SERVERS: readonly string[] = optionalForumMcpServers;
474
+
475
+ function refreshForumCatalogViews(): void {
476
+ const all = namesInScope("forum");
477
+ const required = Object.entries(MCP_CATALOG)
478
+ .filter(([, entry]) => entry.scopes.includes("forum") && entry.forumRequired)
479
+ .map(([name]) => name);
480
+ allForumMcpServerNames.splice(0, allForumMcpServerNames.length, ...all);
481
+ requiredForumMcpServers.splice(0, requiredForumMcpServers.length, ...required);
482
+ optionalForumMcpServers.splice(
483
+ 0,
484
+ optionalForumMcpServers.length,
485
+ ...all.filter((name) => !required.includes(name)),
486
+ );
487
+ }
488
+
489
+ refreshForumCatalogViews();
490
+
491
+ /**
492
+ * Mount one MCP capability supplied by an optional node module.
493
+ *
494
+ * Registration happens once at node startup, never in the per-event hot path.
495
+ * The returned cleanup closure only removes the exact entry installed by this
496
+ * call, making module stop/restart safe.
497
+ */
498
+ export function registerRuntimeMcpServer(name: string, entry: RuntimeMcpCatalogEntry): () => void {
499
+ const key = name.trim();
500
+ if (!key || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(key)) {
501
+ throw new Error(`invalid runtime MCP server name: ${name}`);
502
+ }
503
+ if (MCP_CATALOG[key]) throw new Error(`runtime MCP server already registered: ${key}`);
504
+ MCP_CATALOG[key] = entry;
505
+ refreshForumCatalogViews();
506
+ return () => {
507
+ if (MCP_CATALOG[key] !== entry) return;
508
+ delete MCP_CATALOG[key];
509
+ refreshForumCatalogViews();
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Format the active MCP config for display (used by get_mcp_config /
515
+ * get_topic_mcp / configure_mcp response).
516
+ *
517
+ * In whitelist mode the DB stores only the user-chosen optional servers;
518
+ * required servers are always active but never written to the whitelist.
519
+ * This helper surfaces that split so the model never confuses "not in
520
+ * whitelist" with "not active".
521
+ */
522
+ export function formatMcpStatus(config: {
523
+ enabled: string[] | null;
524
+ extra?: Record<string, unknown>;
525
+ }): string[] {
526
+ const extraNames = Object.keys(config.extra ?? {});
527
+
528
+ if (config.enabled === null) {
529
+ const total = ALL_FORUM_MCP_SERVER_NAMES.length + extraNames.length;
530
+ return [
531
+ `설정 방식: 기본값 (전체 활성)`,
532
+ `활성 서버 (${total}개): ${[...ALL_FORUM_MCP_SERVER_NAMES, ...extraNames].join(", ")}`,
533
+ `추가 서버: ${extraNames.length > 0 ? extraNames.join(", ") : "없음"}`,
534
+ ];
535
+ }
536
+
537
+ // whitelist mode: stored list may or may not include required servers
538
+ // (old entries might have them; new entries won't). Either way, split
539
+ // by required vs optional for display.
540
+ const optionalActive = config.enabled.filter((n) => !REQUIRED_FORUM_MCP_SERVERS.includes(n));
541
+ const total = REQUIRED_FORUM_MCP_SERVERS.length + optionalActive.length + extraNames.length;
542
+ return [
543
+ `설정 방식: whitelist`,
544
+ `필수 서버 (항상 활성, ${REQUIRED_FORUM_MCP_SERVERS.length}개): ${REQUIRED_FORUM_MCP_SERVERS.join(", ")}`,
545
+ `선택 서버 (whitelist, ${optionalActive.length}개): ${optionalActive.length > 0 ? optionalActive.join(", ") : "없음"}`,
546
+ `실제 활성 합계: ${total}개`,
547
+ `추가 서버: ${extraNames.length > 0 ? extraNames.join(", ") : "없음"}`,
548
+ ``,
549
+ `선택 가능 서버 전체 (${OPTIONAL_FORUM_MCP_SERVERS.length}개): ${OPTIONAL_FORUM_MCP_SERVERS.join(", ")}`,
550
+ ];
551
+ }
552
+
553
+ // --- Builders for each scope ---
554
+
555
+ // ── Node-assigned MCP servers (mcp-host manifest) ───────────────────
556
+ //
557
+ // The per-node MCP manifest (`negotium mcp add`) lives in @negotium/mcp-host,
558
+ // which depends on core — so core cannot read it directly. Instead the host
559
+ // process resolves the manifest at node start (ensuring long-lived http
560
+ // servers are running and have ports) and installs the result here. Entries
561
+ // then ride every eligible scope's catalog, subject to the same per-topic
562
+ // `enabled` whitelist as optional built-ins.
563
+
564
+ export type NodeMcpEntry =
565
+ | { key: string; kind: "http"; port: number }
566
+ | { key: string; kind: "stdio"; command: string; args?: string[]; env?: Record<string, string> };
567
+
568
+ let nodeMcpEntries: NodeMcpEntry[] = [];
569
+
570
+ /** Install the node's assigned MCP servers. Last writer wins (host restart-safe). */
571
+ export function setNodeMcpServers(entries: NodeMcpEntry[]): void {
572
+ nodeMcpEntries = entries.filter((entry) => !(entry.key in MCP_CATALOG));
573
+ const dropped = entries.length - nodeMcpEntries.length;
574
+ if (dropped > 0) {
575
+ logger.warn({ dropped }, "setNodeMcpServers: entries shadowing built-in catalog keys ignored");
576
+ }
577
+ }
578
+
579
+ export function getNodeMcpServers(): readonly NodeMcpEntry[] {
580
+ return nodeMcpEntries;
581
+ }
582
+
583
+ function buildNodeMcpSpecs(
584
+ agent: AgentKind | undefined,
585
+ filter: (name: string) => boolean,
586
+ ): Record<string, unknown> {
587
+ const out: Record<string, unknown> = {};
588
+ for (const entry of nodeMcpEntries) {
589
+ if (!filter(entry.key)) continue;
590
+ out[entry.key] =
591
+ entry.kind === "http"
592
+ ? longLivedHttpMcp(agent, entry.port)
593
+ : {
594
+ command: entry.command,
595
+ args: entry.args ?? [],
596
+ ...(entry.env ? { env: entry.env } : {}),
597
+ };
598
+ }
599
+ return out;
600
+ }
601
+
602
+ function buildScope(
603
+ scope: RuntimeMcpScope,
604
+ ctx: RuntimeMcpBuildContext,
605
+ filter: (name: string) => boolean = () => true,
606
+ ): Record<string, unknown> {
607
+ const out: Record<string, unknown> = {};
608
+ for (const [name, entry] of Object.entries(MCP_CATALOG)) {
609
+ if (!entry.scopes.includes(scope)) continue;
610
+ if (!filter(name)) continue;
611
+ // `null` from build() means "skip this server for this turn" —
612
+ // currently used by the playwright entry when no long-lived port
613
+ // is available.
614
+ const spec = entry.build(ctx);
615
+ if (spec === null) continue;
616
+ out[name] = spec;
617
+ }
618
+ // Node-assigned MCPs join every scope except cron (scheduled runs keep the
619
+ // deliberately narrow built-in set).
620
+ if (scope !== "cron") {
621
+ Object.assign(out, buildNodeMcpSpecs(ctx.agent, filter));
622
+ }
623
+ return out;
624
+ }
625
+
626
+ /** DM session: catalog entries with `dm` scope, no whitelist. */
627
+ export function getDmMcpServers(opts: {
628
+ userId: string;
629
+ agent?: AgentKind;
630
+ playwrightPort?: number;
631
+ playwrightCapability?: string;
632
+ }) {
633
+ return buildScope("dm", {
634
+ userId: opts.userId,
635
+ session: "dm",
636
+ agent: opts.agent,
637
+ playwrightPort: opts.playwrightPort,
638
+ playwrightCapability: opts.playwrightCapability,
639
+ });
640
+ }
641
+
642
+ /**
643
+ * Manager session: lives in the caller's private General API topic. Owns topic CRUD,
644
+ * invite/user admin, and cross-topic MCP toggling through `topic-admin`,
645
+ * plus general utilities. Browser/OCR work belongs in dedicated topics, so
646
+ * heavyweight Playwright/PaddleOCR tools are intentionally excluded here.
647
+ * No whitelist — manager-scope catalog entries are loaded as-is.
648
+ */
649
+ export function getManagerMcpServers(opts: {
650
+ userId: string;
651
+ session?: string;
652
+ topicId?: string;
653
+ queryId?: string;
654
+ wikiTopicId?: string;
655
+ agent?: AgentKind;
656
+ cwd?: string;
657
+ model?: string;
658
+ currentUserPrompt?: string;
659
+ playwrightPort?: number;
660
+ playwrightCapability?: string;
661
+ autoContinue?: boolean;
662
+ }) {
663
+ if (!opts.topicId) {
664
+ throw new Error("getManagerMcpServers: private General topicId is required");
665
+ }
666
+ const topicId = opts.topicId;
667
+ return buildScope("manager", {
668
+ userId: opts.userId,
669
+ session: opts.session ?? "General",
670
+ topicId,
671
+ queryId: opts.queryId,
672
+ wikiTopicId: opts.wikiTopicId ?? topicId,
673
+ agent: opts.agent,
674
+ cwd: opts.cwd,
675
+ model: opts.model,
676
+ currentUserPrompt: opts.currentUserPrompt,
677
+ playwrightPort: opts.playwrightPort,
678
+ playwrightCapability: opts.playwrightCapability,
679
+ autoContinue: opts.autoContinue,
680
+ });
681
+ }
682
+
683
+ /**
684
+ * Forum session: catalog entries with `forum` scope.
685
+ * - `enabled === null` → all forum servers.
686
+ * - `enabled` is an array → whitelist; required servers are always included.
687
+ * - `extra` is merged on top, allowing one-off injections per topic.
688
+ */
689
+ export function getForumMcpServers(opts: {
690
+ userId: string;
691
+ session: string;
692
+ topicId?: string;
693
+ queryId?: string;
694
+ wikiTopicId?: string;
695
+ agent: AgentKind;
696
+ cwd?: string;
697
+ model?: string;
698
+ currentUserPrompt?: string;
699
+ playwrightPort?: number;
700
+ playwrightCapability?: string;
701
+ bgBashPort?: number;
702
+ autoContinue?: boolean;
703
+ depth?: number;
704
+ enabled?: string[] | null;
705
+ extra?: Record<string, unknown>;
706
+ silent?: boolean;
707
+ peerBridge?: PeerRuntimeBridgeContext;
708
+ }) {
709
+ const {
710
+ userId,
711
+ session,
712
+ topicId,
713
+ queryId,
714
+ wikiTopicId,
715
+ agent,
716
+ cwd,
717
+ model,
718
+ currentUserPrompt,
719
+ playwrightPort,
720
+ playwrightCapability,
721
+ depth = 0,
722
+ enabled = null,
723
+ extra = {},
724
+ silent = false,
725
+ bgBashPort,
726
+ autoContinue,
727
+ peerBridge,
728
+ } = opts;
729
+
730
+ const filter = (name: string) => {
731
+ if (silent && name === "task") return false;
732
+ if (enabled === null) return true;
733
+ return (
734
+ enabled.includes(name) || (REQUIRED_FORUM_MCP_SERVERS as readonly string[]).includes(name)
735
+ );
736
+ };
737
+
738
+ const base = buildScope(
739
+ "forum",
740
+ {
741
+ userId,
742
+ session,
743
+ topicId,
744
+ queryId,
745
+ wikiTopicId,
746
+ agent,
747
+ cwd,
748
+ model,
749
+ currentUserPrompt,
750
+ depth,
751
+ playwrightPort,
752
+ playwrightCapability,
753
+ bgBashPort,
754
+ autoContinue,
755
+ silent,
756
+ peerBridge,
757
+ },
758
+ filter,
759
+ );
760
+
761
+ return { ...base, ...extra };
762
+ }
763
+
764
+ /**
765
+ * Cron session: scheduled background run for a concrete API topic. Keep the
766
+ * catalog narrower than forum scope: runtime delivery, topic memory/status,
767
+ * OCR/vault utilities, and a caller-provided disposable Playwright port.
768
+ */
769
+ export function getCronMcpServers(opts: {
770
+ userId: string;
771
+ session: string;
772
+ topicId: string;
773
+ queryId?: string;
774
+ wikiTopicId?: string;
775
+ agent: AgentKind;
776
+ cwd?: string;
777
+ model?: string;
778
+ currentUserPrompt?: string;
779
+ playwrightPort?: number;
780
+ playwrightCapability?: string;
781
+ }) {
782
+ return buildScope("cron", {
783
+ userId: opts.userId,
784
+ session: opts.session,
785
+ topicId: opts.topicId,
786
+ queryId: opts.queryId,
787
+ wikiTopicId: opts.wikiTopicId ?? opts.topicId,
788
+ agent: opts.agent,
789
+ cwd: opts.cwd,
790
+ model: opts.model,
791
+ currentUserPrompt: opts.currentUserPrompt,
792
+ playwrightPort: opts.playwrightPort,
793
+ playwrightCapability: opts.playwrightCapability,
794
+ autoContinue: false,
795
+ });
796
+ }
797
+
798
+ /**
799
+ * Pick the right MCP server bundle for an agent query. DM/ephemeral sessions
800
+ * get the DM bundle; forum sessions get the forum bundle with full options
801
+ * forwarded. Returned shape is the Claude-SDK shape; Codex callers funnel it
802
+ * through their own translator (`toCodexMcpServers`).
803
+ */
804
+ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, unknown> {
805
+ if (opts.sessionType === "cron") {
806
+ if (!opts.topicId) throw new Error("getMcpServersForQuery: cron sessionType requires topicId");
807
+ return getCronMcpServers({
808
+ userId: opts.userId || "default",
809
+ session: opts.session || "cron",
810
+ topicId: opts.topicId,
811
+ queryId: opts.queryId,
812
+ wikiTopicId: opts.wikiTopicId,
813
+ agent: opts.agent,
814
+ cwd: opts.cwd,
815
+ model: opts.model,
816
+ currentUserPrompt: opts.prompt,
817
+ playwrightPort: opts.playwrightPort,
818
+ playwrightCapability: opts.playwrightCapability,
819
+ });
820
+ }
821
+ if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
822
+ return getDmMcpServers({
823
+ userId: opts.userId || "default",
824
+ agent: opts.agent,
825
+ playwrightPort: opts.playwrightPort,
826
+ playwrightCapability: opts.playwrightCapability,
827
+ });
828
+ }
829
+ if (opts.sessionType === "manager") {
830
+ return getManagerMcpServers({
831
+ userId: opts.userId || "default",
832
+ session: opts.session,
833
+ topicId: opts.topicId,
834
+ queryId: opts.queryId,
835
+ wikiTopicId: opts.wikiTopicId,
836
+ agent: opts.agent,
837
+ cwd: opts.cwd,
838
+ model: opts.model,
839
+ currentUserPrompt: opts.prompt,
840
+ playwrightPort: opts.playwrightPort,
841
+ playwrightCapability: opts.playwrightCapability,
842
+ autoContinue: opts.autoContinue,
843
+ });
844
+ }
845
+ return getForumMcpServers({
846
+ userId: opts.userId || "default",
847
+ session: opts.session || "default",
848
+ topicId: opts.topicId,
849
+ queryId: opts.queryId,
850
+ wikiTopicId: opts.wikiTopicId,
851
+ agent: opts.agent,
852
+ cwd: opts.cwd,
853
+ model: opts.model,
854
+ currentUserPrompt: opts.prompt,
855
+ playwrightPort: opts.playwrightPort,
856
+ playwrightCapability: opts.playwrightCapability,
857
+ bgBashPort: opts.bgBashPort,
858
+ autoContinue: opts.autoContinue,
859
+ depth: opts.depth,
860
+ enabled: opts.mcpEnabled,
861
+ extra: opts.mcpExtra,
862
+ silent: opts.silent,
863
+ peerBridge: opts.peerBridge,
864
+ });
865
+ }