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,1331 @@
1
+ import {
2
+ GENERAL_TOPIC_ID,
3
+ getManagerTopicForUser,
4
+ upsertTopic
5
+ } from "./chunk-xfq68rch.js";
6
+ import {
7
+ getApiTopicConfig,
8
+ setApiTopicConfig
9
+ } from "./chunk-gmy11efb.js";
10
+ import {
11
+ db
12
+ } from "./chunk-7xqj7aex.js";
13
+ import {
14
+ getRegistry
15
+ } from "./chunk-5ahkew9c.js";
16
+ import {
17
+ sanitizeTopicName
18
+ } from "./chunk-6cpn7ht2.js";
19
+ import {
20
+ BROWSER_PROFILES_DIR,
21
+ PATCHRIGHT_MCP_BIN,
22
+ PLAYWRIGHT_BASE_PORT,
23
+ PLAYWRIGHT_MAX_PORT,
24
+ PLAYWRIGHT_MCP_BIN,
25
+ PLAYWRIGHT_PORTS_DIR,
26
+ logger,
27
+ resolveBrowserProxy,
28
+ resolveDefaultModel
29
+ } from "./chunk-k9z47mab.js";
30
+
31
+ // ../../packages/core/src/storage/runtime-events.ts
32
+ var RUNTIME_EVENT_TYPES = [
33
+ "message",
34
+ "message-updated",
35
+ "ai-status",
36
+ "topic-created",
37
+ "topic-updated",
38
+ "topic-deleted"
39
+ ];
40
+ db.exec(`
41
+ CREATE TABLE IF NOT EXISTS runtime_events (
42
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ source_id TEXT NOT NULL,
44
+ event_type TEXT NOT NULL CHECK (
45
+ event_type IN (
46
+ 'message',
47
+ 'message-updated',
48
+ 'ai-status',
49
+ 'topic-created',
50
+ 'topic-updated',
51
+ 'topic-deleted'
52
+ )
53
+ ),
54
+ topic_id TEXT NOT NULL,
55
+ payload_json TEXT NOT NULL,
56
+ created_at TEXT NOT NULL
57
+ )
58
+ `);
59
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_events_topic_seq ON runtime_events(topic_id, seq)");
60
+ var types = new Set(RUNTIME_EVENT_TYPES);
61
+ function rowToEvent(row) {
62
+ if (!types.has(row.event_type))
63
+ return null;
64
+ try {
65
+ return {
66
+ seq: Number(row.seq),
67
+ sourceId: row.source_id,
68
+ type: row.event_type,
69
+ topicId: row.topic_id,
70
+ payload: JSON.parse(row.payload_json),
71
+ createdAt: row.created_at
72
+ };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function latestRuntimeEventSeq() {
78
+ const row = db.query("SELECT MAX(seq) AS seq FROM runtime_events").get();
79
+ return Number(row?.seq ?? 0);
80
+ }
81
+ function appendRuntimeEvent(sourceId, event) {
82
+ const createdAt = new Date().toISOString();
83
+ const result = db.query(`INSERT INTO runtime_events
84
+ (source_id, event_type, topic_id, payload_json, created_at)
85
+ VALUES (?, ?, ?, ?, ?)`).run(sourceId, event.type, event.topicId, JSON.stringify(event.payload ?? null), createdAt);
86
+ const seq = Number(result.lastInsertRowid);
87
+ return { seq, sourceId, ...event, createdAt };
88
+ }
89
+ function listRuntimeEventsAfter(seq, limit = 500) {
90
+ const rows = db.query(`SELECT seq, source_id, event_type, topic_id, payload_json, created_at
91
+ FROM runtime_events
92
+ WHERE seq > ?
93
+ ORDER BY seq ASC
94
+ LIMIT ?`).all(Math.max(0, seq), Math.max(1, Math.min(limit, 5000)));
95
+ return rows.map(rowToEvent).filter((event) => event !== null);
96
+ }
97
+ function listRecentRuntimeEventsForTopic(topicId, limit = 300) {
98
+ const rows = db.query(`SELECT seq, source_id, event_type, topic_id, payload_json, created_at
99
+ FROM (
100
+ SELECT seq, source_id, event_type, topic_id, payload_json, created_at
101
+ FROM runtime_events
102
+ WHERE topic_id = ? AND event_type IN ('ai-status', 'message-updated')
103
+ ORDER BY seq DESC
104
+ LIMIT ?
105
+ )
106
+ ORDER BY seq ASC`).all(topicId, Math.max(1, Math.min(limit, 2000)));
107
+ return rows.map(rowToEvent).filter((event) => event !== null);
108
+ }
109
+
110
+ // ../../packages/core/src/bus.ts
111
+ import { randomUUID } from "crypto";
112
+ class SqliteRuntimeBus {
113
+ listeners = new Set;
114
+ sourceId;
115
+ pollIntervalMs;
116
+ cursor;
117
+ pollTimer = null;
118
+ polling = false;
119
+ constructor(options = {}) {
120
+ this.sourceId = options.sourceId ?? `${process.pid}-${randomUUID()}`;
121
+ this.pollIntervalMs = Math.max(25, options.pollIntervalMs ?? 100);
122
+ this.cursor = latestRuntimeEventSeq();
123
+ }
124
+ deliver(event) {
125
+ for (const listener of this.listeners) {
126
+ try {
127
+ listener(event);
128
+ } catch {}
129
+ }
130
+ }
131
+ emit(event) {
132
+ const stored = appendRuntimeEvent(this.sourceId, event);
133
+ this.deliver({ ...event, seq: stored.seq, createdAt: stored.createdAt });
134
+ }
135
+ startPolling() {
136
+ if (this.pollTimer)
137
+ return;
138
+ this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
139
+ this.pollTimer.unref?.();
140
+ this.poll();
141
+ }
142
+ stopPolling() {
143
+ if (!this.pollTimer)
144
+ return;
145
+ clearInterval(this.pollTimer);
146
+ this.pollTimer = null;
147
+ }
148
+ poll() {
149
+ if (this.polling || this.listeners.size === 0)
150
+ return;
151
+ this.polling = true;
152
+ try {
153
+ while (true) {
154
+ const events = listRuntimeEventsAfter(this.cursor);
155
+ if (events.length === 0)
156
+ break;
157
+ for (const event of events) {
158
+ this.cursor = Math.max(this.cursor, event.seq);
159
+ if (event.sourceId === this.sourceId)
160
+ continue;
161
+ this.deliver({
162
+ type: event.type,
163
+ topicId: event.topicId,
164
+ payload: event.payload,
165
+ seq: event.seq,
166
+ createdAt: event.createdAt
167
+ });
168
+ }
169
+ if (events.length < 500)
170
+ break;
171
+ }
172
+ } finally {
173
+ this.polling = false;
174
+ }
175
+ }
176
+ broadcastMessage(topicId, message) {
177
+ this.emit({ type: "message", topicId, payload: message });
178
+ }
179
+ broadcastMessageUpdated(topicId, messageId, patch) {
180
+ this.emit({ type: "message-updated", topicId, payload: { messageId, patch } });
181
+ }
182
+ broadcastAiStatus(topicId, status) {
183
+ this.emit({ type: "ai-status", topicId, payload: status });
184
+ }
185
+ broadcastTopicCreated(topic) {
186
+ this.emit({ type: "topic-created", topicId: topic.id, payload: topic });
187
+ }
188
+ broadcastTopicUpdated(topicId) {
189
+ this.emit({ type: "topic-updated", topicId, payload: null });
190
+ }
191
+ broadcastTopicDeleted(topicId) {
192
+ this.emit({ type: "topic-deleted", topicId, payload: null });
193
+ }
194
+ broadcastAiActive(topicId, queryId) {
195
+ this.broadcastAiStatus(topicId, { kind: "ai_active", queryId });
196
+ }
197
+ broadcastDone(topicId, queryId, usage, meta) {
198
+ this.broadcastAiStatus(topicId, {
199
+ kind: "ai_done",
200
+ queryId,
201
+ usage,
202
+ agent: meta?.agent,
203
+ model: meta?.model
204
+ });
205
+ }
206
+ broadcastError(topicId, queryId, error) {
207
+ this.broadcastAiStatus(topicId, { kind: "ai_error", queryId, error });
208
+ }
209
+ broadcastAborted(topicId, queryId, reason) {
210
+ this.broadcastAiStatus(topicId, { kind: "ai_aborted", queryId, reason });
211
+ }
212
+ broadcastToolCall(topicId, queryId, name, input, label, toolUseId) {
213
+ const id = toolUseId.trim() || `${queryId}:tool`;
214
+ this.broadcastAiStatus(topicId, {
215
+ kind: "tool_call",
216
+ queryId,
217
+ name,
218
+ input,
219
+ label,
220
+ toolUseId: id
221
+ });
222
+ }
223
+ broadcastToolOutput(topicId, queryId, toolUseId, content) {
224
+ const id = toolUseId.trim() || `${queryId}:tool`;
225
+ this.broadcastAiStatus(topicId, { kind: "tool_output", queryId, toolUseId: id, content });
226
+ }
227
+ broadcastToolStatus(topicId, queryId, kind, content, meta) {
228
+ this.broadcastAiStatus(topicId, {
229
+ kind: "tool_status",
230
+ queryId,
231
+ statusKind: kind,
232
+ content,
233
+ toolName: meta?.toolName,
234
+ elapsed: meta?.elapsed
235
+ });
236
+ }
237
+ broadcastFileReady(topicId, queryId, path, source) {
238
+ this.broadcastAiStatus(topicId, { kind: "file_ready", queryId, path, source });
239
+ }
240
+ broadcastReasoning(topicId, queryId, content) {
241
+ this.broadcastAiStatus(topicId, { kind: "reasoning", queryId, content });
242
+ }
243
+ broadcastVisual(topicId, queryId, url, id, title, kind) {
244
+ this.broadcastAiStatus(topicId, { kind: "visual", queryId, url, id, title, visualKind: kind });
245
+ }
246
+ broadcastTyping(topicId, userId) {
247
+ this.broadcastAiStatus(topicId, { kind: "typing", userId });
248
+ }
249
+ subscribe(listener) {
250
+ this.listeners.add(listener);
251
+ this.startPolling();
252
+ return () => {
253
+ this.listeners.delete(listener);
254
+ if (this.listeners.size === 0)
255
+ this.stopPolling();
256
+ };
257
+ }
258
+ }
259
+ var current = new SqliteRuntimeBus;
260
+ function runtimeBus() {
261
+ return current;
262
+ }
263
+ function setRuntimeBus(bus) {
264
+ current = bus;
265
+ }
266
+ var WsHub = {
267
+ get() {
268
+ return current;
269
+ }
270
+ };
271
+
272
+ // ../../packages/core/src/storage/api-messages.ts
273
+ db.exec(`
274
+ CREATE TABLE IF NOT EXISTS api_messages (
275
+ id TEXT PRIMARY KEY,
276
+ topic_id TEXT NOT NULL,
277
+ parent_id TEXT,
278
+ author_id TEXT NOT NULL,
279
+ source_adapter TEXT,
280
+ text TEXT NOT NULL,
281
+ query_id TEXT,
282
+ agent_type TEXT,
283
+ model TEXT,
284
+ attachments TEXT,
285
+ usage TEXT,
286
+ deleted INTEGER NOT NULL DEFAULT 0,
287
+ edited_at TEXT,
288
+ reactions TEXT,
289
+ kind TEXT,
290
+ ask_user_question TEXT,
291
+ mentions TEXT,
292
+ thread_root_id TEXT,
293
+ created_at TEXT NOT NULL
294
+ )
295
+ `);
296
+ try {
297
+ db.exec("ALTER TABLE api_messages ADD COLUMN source_adapter TEXT");
298
+ } catch {}
299
+ try {
300
+ db.exec("ALTER TABLE api_messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0");
301
+ } catch {}
302
+ try {
303
+ db.exec("ALTER TABLE api_messages ADD COLUMN edited_at TEXT");
304
+ } catch {}
305
+ try {
306
+ db.exec("ALTER TABLE api_messages ADD COLUMN reactions TEXT");
307
+ } catch {}
308
+ try {
309
+ db.exec("ALTER TABLE api_messages ADD COLUMN kind TEXT");
310
+ } catch {}
311
+ try {
312
+ db.exec("ALTER TABLE api_messages ADD COLUMN ask_user_question TEXT");
313
+ } catch {}
314
+ try {
315
+ db.exec("ALTER TABLE api_messages ADD COLUMN mentions TEXT");
316
+ } catch {}
317
+ try {
318
+ db.exec("ALTER TABLE api_messages ADD COLUMN thread_root_id TEXT");
319
+ } catch {}
320
+ try {
321
+ db.exec("ALTER TABLE api_messages ADD COLUMN subagent_card TEXT");
322
+ } catch {}
323
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_topic ON api_messages(topic_id)");
324
+ db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_thread_root ON api_messages(thread_root_id)");
325
+ var appendHooks = new Set;
326
+ function emitApiMessageAppended(msg) {
327
+ for (const hook of appendHooks) {
328
+ queueMicrotask(() => {
329
+ try {
330
+ Promise.resolve(hook(msg)).catch(() => {
331
+ return;
332
+ });
333
+ } catch {}
334
+ });
335
+ }
336
+ }
337
+ function rowToDto(r) {
338
+ return {
339
+ id: r.id,
340
+ topicId: r.topic_id,
341
+ parentId: r.parent_id ?? undefined,
342
+ authorId: r.author_id,
343
+ sourceAdapter: r.source_adapter ?? undefined,
344
+ text: r.text,
345
+ queryId: r.query_id ?? undefined,
346
+ agentType: r.agent_type ?? undefined,
347
+ model: r.model ?? undefined,
348
+ attachments: r.attachments ? JSON.parse(r.attachments) : undefined,
349
+ usage: r.usage ? JSON.parse(r.usage) : undefined,
350
+ deleted: r.deleted !== 0,
351
+ editedAt: r.edited_at ?? undefined,
352
+ reactions: r.reactions ? JSON.parse(r.reactions) : undefined,
353
+ kind: r.kind ?? undefined,
354
+ askUserQuestion: r.ask_user_question ? JSON.parse(r.ask_user_question) : undefined,
355
+ subagentCard: r.subagent_card ? JSON.parse(r.subagent_card) : undefined,
356
+ mentions: r.mentions ? JSON.parse(r.mentions) : undefined,
357
+ threadRootId: r.thread_root_id ?? undefined,
358
+ createdAt: r.created_at
359
+ };
360
+ }
361
+ function attachmentListHasFileId(raw, fileId) {
362
+ if (!raw)
363
+ return false;
364
+ try {
365
+ const attachments = JSON.parse(raw);
366
+ return Array.isArray(attachments) && attachments.some((attachment) => typeof attachment === "object" && attachment !== null && ("id" in attachment) && attachment.id === fileId);
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ function appendApiMessage(msg, options = {}) {
372
+ const notify = options.notify ?? true;
373
+ const updateTopicLastMessageAt = options.updateTopicLastMessageAt ?? true;
374
+ let inserted = false;
375
+ db.transaction(() => {
376
+ const result = db.query(`INSERT INTO api_messages
377
+ (id, topic_id, parent_id, author_id, source_adapter, text, query_id, agent_type, model, attachments, usage, deleted, edited_at, reactions, kind, ask_user_question, subagent_card, mentions, thread_root_id, created_at)
378
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
379
+ ON CONFLICT(id) DO NOTHING`).run(msg.id, msg.topicId, msg.parentId ?? null, msg.authorId, msg.sourceAdapter ?? null, msg.text, msg.queryId ?? null, msg.agentType ?? null, msg.model ?? null, msg.attachments ? JSON.stringify(msg.attachments) : null, msg.usage ? JSON.stringify(msg.usage) : null, msg.deleted ? 1 : 0, msg.editedAt ?? null, msg.reactions?.length ? JSON.stringify(msg.reactions) : null, msg.kind ?? null, msg.askUserQuestion ? JSON.stringify(msg.askUserQuestion) : null, msg.subagentCard ? JSON.stringify(msg.subagentCard) : null, msg.mentions?.length ? JSON.stringify(msg.mentions) : null, msg.threadRootId ?? null, msg.createdAt);
380
+ inserted = Number(result.changes ?? 0) > 0;
381
+ if (inserted && updateTopicLastMessageAt && !msg.deleted) {
382
+ db.query(`UPDATE api_topics
383
+ SET last_message_at = CASE
384
+ WHEN last_message_at IS NULL OR last_message_at < ? THEN ?
385
+ ELSE last_message_at
386
+ END
387
+ WHERE id = ?`).run(msg.createdAt, msg.createdAt, msg.topicId);
388
+ }
389
+ })();
390
+ if (inserted && notify && !msg.deleted)
391
+ emitApiMessageAppended(msg);
392
+ }
393
+ function getApiMessage(topicId, messageId) {
394
+ const row = db.query("SELECT * FROM api_messages WHERE topic_id = ? AND id = ?").get(topicId, messageId);
395
+ return row ? rowToDto(row) : null;
396
+ }
397
+ function topicHasAttachmentFileId(topicId, fileId) {
398
+ const rows = db.query("SELECT attachments FROM api_messages WHERE topic_id = ? AND deleted = 0 AND attachments IS NOT NULL").all(topicId);
399
+ return rows.some((row) => attachmentListHasFileId(row.attachments, fileId));
400
+ }
401
+ function updateApiMessageText(topicId, messageId, text, editedAt = new Date().toISOString()) {
402
+ const res = db.query(`UPDATE api_messages
403
+ SET text = ?, edited_at = ?
404
+ WHERE topic_id = ? AND id = ? AND deleted = 0`).run(text, editedAt, topicId, messageId);
405
+ if (Number(res.changes ?? 0) === 0)
406
+ return null;
407
+ return getApiMessage(topicId, messageId);
408
+ }
409
+ function updateApiMessageUsage(topicId, messageId, usage) {
410
+ const res = db.query(`UPDATE api_messages
411
+ SET usage = ?
412
+ WHERE topic_id = ? AND id = ? AND deleted = 0`).run(JSON.stringify(usage), topicId, messageId);
413
+ if (Number(res.changes ?? 0) === 0)
414
+ return null;
415
+ return getApiMessage(topicId, messageId);
416
+ }
417
+ function updateApiMessageAskUserQuestion(topicId, messageId, askUserQuestion, editedAt = new Date().toISOString()) {
418
+ const res = db.query(`UPDATE api_messages
419
+ SET ask_user_question = ?, edited_at = ?
420
+ WHERE topic_id = ? AND id = ? AND deleted = 0 AND kind = 'ask_user_question'`).run(JSON.stringify(askUserQuestion), editedAt, topicId, messageId);
421
+ if (Number(res.changes ?? 0) === 0)
422
+ return null;
423
+ return getApiMessage(topicId, messageId);
424
+ }
425
+ function updateApiMessageSubagentCard(topicId, messageId, subagentCard, editedAt = new Date().toISOString()) {
426
+ const res = db.query(`UPDATE api_messages
427
+ SET subagent_card = ?, edited_at = ?
428
+ WHERE topic_id = ? AND id = ? AND deleted = 0 AND kind = 'subagent'`).run(JSON.stringify(subagentCard), editedAt, topicId, messageId);
429
+ if (Number(res.changes ?? 0) === 0)
430
+ return null;
431
+ return getApiMessage(topicId, messageId);
432
+ }
433
+ function listApiMessagesByKind(kind) {
434
+ const rows = db.query("SELECT * FROM api_messages WHERE kind = ? AND deleted = 0").all(kind);
435
+ return rows.map(rowToDto);
436
+ }
437
+ function softDeleteApiMessage(topicId, messageId) {
438
+ const res = db.query(`UPDATE api_messages
439
+ SET deleted = 1, text = ''
440
+ WHERE topic_id = ? AND id = ? AND deleted = 0`).run(topicId, messageId);
441
+ if (Number(res.changes ?? 0) === 0)
442
+ return null;
443
+ return getApiMessage(topicId, messageId);
444
+ }
445
+ function listApiMessages(topicId, options) {
446
+ const limit = Math.max(1, Math.min(options?.limit ?? 50, 200));
447
+ let anchorRowid = null;
448
+ if (options?.cursor) {
449
+ const cur = db.query("SELECT rowid AS rowid FROM api_messages WHERE topic_id = ? AND id = ?").get(topicId, options.cursor);
450
+ if (cur)
451
+ anchorRowid = cur.rowid;
452
+ }
453
+ const rows = anchorRowid !== null ? db.query(`SELECT *, rowid AS rowid FROM api_messages
454
+ WHERE topic_id = ? AND deleted = 0 AND thread_root_id IS NULL AND rowid < ?
455
+ ORDER BY rowid DESC
456
+ LIMIT ?`).all(topicId, anchorRowid, limit + 1) : db.query(`SELECT *, rowid AS rowid FROM api_messages
457
+ WHERE topic_id = ? AND deleted = 0 AND thread_root_id IS NULL
458
+ ORDER BY rowid DESC
459
+ LIMIT ?`).all(topicId, limit + 1);
460
+ const hasMore = rows.length > limit;
461
+ const pageRows = (hasMore ? rows.slice(0, limit) : rows).reverse();
462
+ const page = pageRows.map(rowToDto);
463
+ const summaries = getThreadSummaries(page.map((m) => m.id));
464
+ if (summaries.size > 0) {
465
+ for (const m of page) {
466
+ const s = summaries.get(m.id);
467
+ if (s) {
468
+ m.threadReplyCount = s.replyCount;
469
+ m.threadLastReplyAt = s.lastReplyAt;
470
+ }
471
+ }
472
+ }
473
+ return {
474
+ page,
475
+ cursor: page.length > 0 ? page[0].id : undefined,
476
+ hasMore
477
+ };
478
+ }
479
+ function getThreadSummaries(rootIds) {
480
+ const out = new Map;
481
+ const ids = [...new Set(rootIds)].filter(Boolean);
482
+ if (ids.length === 0)
483
+ return out;
484
+ const rows = db.query(`SELECT thread_root_id AS rootId, COUNT(*) AS replyCount, MAX(created_at) AS lastReplyAt
485
+ FROM api_messages
486
+ WHERE thread_root_id IN (${ids.map(() => "?").join(",")}) AND deleted = 0
487
+ GROUP BY thread_root_id`).all(...ids);
488
+ for (const r of rows) {
489
+ out.set(r.rootId, { replyCount: Number(r.replyCount), lastReplyAt: r.lastReplyAt });
490
+ }
491
+ return out;
492
+ }
493
+ function deleteMessagesForTopic(topicId) {
494
+ const res = db.query("DELETE FROM api_messages WHERE topic_id = ?").run(topicId);
495
+ return Number(res.changes ?? 0);
496
+ }
497
+ function copyMessagesForTopic(sourceTopicId, targetTopicId) {
498
+ const rows = db.query("SELECT * FROM api_messages WHERE topic_id = ? AND deleted = 0 ORDER BY rowid ASC").all(sourceTopicId);
499
+ if (rows.length === 0)
500
+ return 0;
501
+ const idMap = new Map(rows.map((r) => [r.id, crypto.randomUUID()]));
502
+ let copied = 0;
503
+ for (const r of rows) {
504
+ const newId = idMap.get(r.id);
505
+ if (!newId)
506
+ continue;
507
+ appendApiMessage({
508
+ ...rowToDto(r),
509
+ id: newId,
510
+ topicId: targetTopicId,
511
+ parentId: r.parent_id ? idMap.get(r.parent_id) ?? r.parent_id : undefined,
512
+ threadRootId: r.thread_root_id ? idMap.get(r.thread_root_id) ?? r.thread_root_id : undefined
513
+ }, { notify: false, updateTopicLastMessageAt: false });
514
+ copied++;
515
+ }
516
+ return copied;
517
+ }
518
+ function getAllMessagesForTopic(topicId) {
519
+ return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND deleted = 0 ORDER BY rowid ASC").all(topicId);
520
+ }
521
+ function getMessagesForTopicAfterRowid(topicId, afterRowid) {
522
+ return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND rowid > ? AND deleted = 0 ORDER BY rowid ASC").all(topicId, afterRowid);
523
+ }
524
+
525
+ // ../../packages/core/src/platform/delay.ts
526
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
527
+
528
+ // ../../packages/core/src/storage/browser-profiles.ts
529
+ var DEFAULT_BROWSER_PROFILE = "default";
530
+ function normalizeBrowserProfileName(name) {
531
+ const value = name.trim().toLowerCase();
532
+ if (!/^[a-z0-9][a-z0-9_-]{0,47}$/.test(value)) {
533
+ throw new Error("Profile name must be 1-48 lowercase letters, numbers, '-' or '_'.");
534
+ }
535
+ return value;
536
+ }
537
+ db.exec(`
538
+ CREATE TABLE IF NOT EXISTS browser_profiles (
539
+ owner_id TEXT NOT NULL,
540
+ name TEXT NOT NULL,
541
+ created_at TEXT NOT NULL,
542
+ PRIMARY KEY (owner_id, name)
543
+ )
544
+ `);
545
+ function getBrowserProfileOwner(topicId, fallbackUserId) {
546
+ return db.query("SELECT browser_profile_owner FROM api_topics WHERE id = ?").get(topicId)?.browser_profile_owner ?? fallbackUserId;
547
+ }
548
+ function isTopicBrowserProfileOwner(topicId, userId) {
549
+ return getBrowserProfileOwner(topicId, "") === userId;
550
+ }
551
+ function getTopicBrowserProfile(topicId) {
552
+ const value = db.query("SELECT browser_profile FROM api_topics WHERE id = ?").get(topicId)?.browser_profile;
553
+ return value || DEFAULT_BROWSER_PROFILE;
554
+ }
555
+ function hasBrowserProfileTopic(topicId) {
556
+ return Boolean(db.query("SELECT id FROM api_topics WHERE id = ?").get(topicId));
557
+ }
558
+ function createBrowserProfile(ownerId, rawName) {
559
+ const name = normalizeBrowserProfileName(rawName);
560
+ if (name === DEFAULT_BROWSER_PROFILE)
561
+ return name;
562
+ db.query("INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)").run(ownerId, name, new Date().toISOString());
563
+ return name;
564
+ }
565
+ function assignTopicBrowserProfile(opts) {
566
+ const actualOwner = getBrowserProfileOwner(opts.topicId, "");
567
+ if (!actualOwner)
568
+ throw new Error(`Topic "${opts.topicId}" not found or has no owner.`);
569
+ if (actualOwner !== opts.actorUserId) {
570
+ throw new Error(`Only the topic owner can change its browser profile.`);
571
+ }
572
+ const profile = normalizeBrowserProfileName(opts.profile);
573
+ if (profile !== DEFAULT_BROWSER_PROFILE)
574
+ createBrowserProfile(actualOwner, profile);
575
+ const previous = getTopicBrowserProfile(opts.topicId);
576
+ const result = db.query("UPDATE api_topics SET browser_profile = ? WHERE id = ?").run(profile, opts.topicId);
577
+ if (Number(result.changes ?? 0) !== 1)
578
+ throw new Error(`Topic "${opts.topicId}" not found.`);
579
+ return { previous, profile };
580
+ }
581
+
582
+ // ../../packages/core/src/platform/playwright/manager.ts
583
+ import { execFileSync, spawn } from "child_process";
584
+ import { createHash, randomBytes } from "crypto";
585
+ import {
586
+ cpSync,
587
+ existsSync,
588
+ mkdirSync,
589
+ readdirSync,
590
+ readFileSync,
591
+ renameSync,
592
+ rmSync,
593
+ unlinkSync,
594
+ writeFileSync
595
+ } from "fs";
596
+ import { dirname, join, resolve } from "path";
597
+ function makeInstanceKey(userId, topic) {
598
+ if (!topic)
599
+ return makeBrowserProfileInstanceKey(userId, "default");
600
+ const ownerId = getBrowserProfileOwner(topic, userId);
601
+ const profile = migrateLegacyTopicProfile(ownerId, topic);
602
+ return makeBrowserProfileInstanceKey(ownerId, profile);
603
+ }
604
+ function makeBrowserProfileInstanceKey(ownerId, rawProfile) {
605
+ return `profile:${encodeURIComponent(ownerId)}:${normalizeBrowserProfileName(rawProfile)}`;
606
+ }
607
+ function legacyBrowserProfileName(topic) {
608
+ return `legacy_${createHash("sha256").update(topic).digest("hex").slice(0, 12)}`;
609
+ }
610
+ function migrateLegacyTopicProfile(ownerId, topic) {
611
+ const current2 = getTopicBrowserProfile(topic);
612
+ if (current2 !== "default" || !hasBrowserProfileTopic(topic))
613
+ return current2;
614
+ const legacyDir = resolve(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
615
+ if (!existsSync(legacyDir))
616
+ return current2;
617
+ const profile = legacyBrowserProfileName(topic);
618
+ const profileDir = resolveProfileDir(ownerId, profile);
619
+ mkdirSync(dirname(profileDir), { recursive: true });
620
+ if (!existsSync(profileDir))
621
+ renameSync(legacyDir, profileDir);
622
+ assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
623
+ logger.info({ ownerId, topic, profile }, "Adopted legacy topic browser profile");
624
+ return profile;
625
+ }
626
+ function parseInstanceKey(instanceKey) {
627
+ const match = /^profile:([^:]+):(.+)$/.exec(instanceKey);
628
+ if (!match)
629
+ return { ownerId: "legacy", profile: sanitizeTopicName(instanceKey) };
630
+ return {
631
+ ownerId: decodeURIComponent(match[1]),
632
+ profile: normalizeBrowserProfileName(match[2])
633
+ };
634
+ }
635
+ function portFileName(instanceKey) {
636
+ return createHash("sha256").update(instanceKey).digest("hex").slice(0, 24);
637
+ }
638
+ function writePortFile(instanceKey, port) {
639
+ try {
640
+ mkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });
641
+ writeFileSync(join(PLAYWRIGHT_PORTS_DIR, portFileName(instanceKey)), String(port));
642
+ } catch (e) {
643
+ logger.warn({ err: e, instanceKey, port }, "Failed to save playwright port file");
644
+ }
645
+ }
646
+ function deletePortFile(instanceKey) {
647
+ try {
648
+ unlinkSync(join(PLAYWRIGHT_PORTS_DIR, portFileName(instanceKey)));
649
+ } catch (e) {
650
+ logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
651
+ }
652
+ }
653
+ var BASE_PORT = PLAYWRIGHT_BASE_PORT;
654
+ var MAX_PORT = PLAYWRIGHT_MAX_PORT;
655
+ var MAX_IDLE_MS = 2 * 60 * 60 * 1000;
656
+ var instances = new Map;
657
+ var usedPorts = new Set;
658
+ var spawning = new Map;
659
+ async function withPlaywrightInstanceMaintenance(rawKeys, operation) {
660
+ const keys = [...new Set(rawKeys)].sort();
661
+ for (;; ) {
662
+ const pending = keys.map((key) => spawning.get(key)).filter((promise) => promise !== undefined);
663
+ if (pending.length > 0) {
664
+ await Promise.allSettled(pending);
665
+ continue;
666
+ }
667
+ let releaseBarrier;
668
+ const barrier = new Promise((resolveBarrier) => {
669
+ releaseBarrier = () => resolveBarrier(null);
670
+ });
671
+ for (const key of keys)
672
+ spawning.set(key, barrier);
673
+ try {
674
+ return await operation();
675
+ } finally {
676
+ for (const key of keys) {
677
+ if (spawning.get(key) === barrier)
678
+ spawning.delete(key);
679
+ }
680
+ releaseBarrier();
681
+ }
682
+ }
683
+ }
684
+ var pinnedInstances = new Map;
685
+ function pinPlaywrightInstance(instanceKey) {
686
+ pinnedInstances.set(instanceKey, (pinnedInstances.get(instanceKey) ?? 0) + 1);
687
+ }
688
+ function unpinPlaywrightInstance(instanceKey) {
689
+ const count = pinnedInstances.get(instanceKey);
690
+ if (count === undefined)
691
+ return;
692
+ if (count <= 1)
693
+ pinnedInstances.delete(instanceKey);
694
+ else
695
+ pinnedInstances.set(instanceKey, count - 1);
696
+ }
697
+ function getPlaywrightCapability(instanceKey) {
698
+ return instances.get(instanceKey)?.capability;
699
+ }
700
+ var _onPlaywrightExit = null;
701
+ function evictIdleInstance() {
702
+ const now = Date.now();
703
+ const oldestKey = selectIdleEvictionKey(instances, pinnedInstances.keys(), spawning.keys(), now, MAX_IDLE_MS);
704
+ if (oldestKey) {
705
+ const instance = instances.get(oldestKey);
706
+ if (!instance)
707
+ return null;
708
+ const idleMin = ((now - instance.lastUsedAt) / 60000).toFixed(0);
709
+ logger.info({ key: oldestKey, idleMin }, "Evicting idle playwright instance");
710
+ killInstance(oldestKey);
711
+ return instance.port;
712
+ }
713
+ return null;
714
+ }
715
+ function selectIdleEvictionKey(candidates, pinnedKeys, busyKeys, now, maxIdleMs) {
716
+ const pinned = new Set(pinnedKeys);
717
+ const busy = new Set(busyKeys);
718
+ let oldest = null;
719
+ for (const [key, instance] of candidates) {
720
+ if (pinned.has(key) || busy.has(key))
721
+ continue;
722
+ if (now - instance.lastUsedAt < maxIdleMs)
723
+ continue;
724
+ if (!oldest || instance.lastUsedAt < oldest.lastUsedAt) {
725
+ oldest = { key, lastUsedAt: instance.lastUsedAt };
726
+ }
727
+ }
728
+ return oldest?.key ?? null;
729
+ }
730
+ async function allocatePort(expectedUserDataDir) {
731
+ for (let port = BASE_PORT;port <= MAX_PORT; port++) {
732
+ if (usedPorts.has(port))
733
+ continue;
734
+ usedPorts.add(port);
735
+ if (isPortInUse(port)) {
736
+ logger.warn({ port }, "Port occupied by external process, attempting cleanup");
737
+ await killPlaywrightOnPort(port, expectedUserDataDir);
738
+ if (isPortInUse(port)) {
739
+ usedPorts.delete(port);
740
+ continue;
741
+ }
742
+ }
743
+ return port;
744
+ }
745
+ const evictedPort = evictIdleInstance();
746
+ if (evictedPort !== null) {
747
+ await waitForPortRelease(evictedPort);
748
+ const reusablePort = selectReusablePort(BASE_PORT, MAX_PORT, usedPorts, isPortInUse);
749
+ if (reusablePort !== null) {
750
+ usedPorts.add(reusablePort);
751
+ return reusablePort;
752
+ }
753
+ }
754
+ throw new Error(`No available ports for Playwright MCP (${instances.size} active instances, range ${BASE_PORT}-${MAX_PORT})`);
755
+ }
756
+ function selectReusablePort(minPort, maxPort, reservedPorts, isOccupied) {
757
+ for (let port = minPort;port <= maxPort; port++) {
758
+ if (reservedPorts.has(port) || isOccupied(port))
759
+ continue;
760
+ return port;
761
+ }
762
+ return null;
763
+ }
764
+ function releasePort(port) {
765
+ usedPorts.delete(port);
766
+ }
767
+ function isPortInUse(port) {
768
+ try {
769
+ execFileSync("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" });
770
+ return true;
771
+ } catch {
772
+ return false;
773
+ }
774
+ }
775
+ function extractUserDataDirArg(cmdline) {
776
+ const m = cmdline.match(/--user-data-dir(?:\s+|=)(\S+)/);
777
+ return m ? m[1] : null;
778
+ }
779
+ async function killPlaywrightOnPort(port, expectedUserDataDir) {
780
+ try {
781
+ const pids = execFileSync("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" }).toString().trim();
782
+ if (!pids)
783
+ return;
784
+ for (const pid of pids.split(`
785
+ `)) {
786
+ try {
787
+ const cmdline = execFileSync("ps", ["-p", pid, "-o", "command="], { stdio: "pipe" }).toString().trim();
788
+ if (!cmdline.includes("mcp-patchright")) {
789
+ logger.warn({ port, pid, cmdline: cmdline.slice(0, 80) }, "Port occupied by non-browser-MCP process, skipping");
790
+ continue;
791
+ }
792
+ if (expectedUserDataDir) {
793
+ const otherDataDir = extractUserDataDirArg(cmdline);
794
+ if (!browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir)) {
795
+ logger.warn({
796
+ port,
797
+ pid,
798
+ otherDataDir,
799
+ expectedUserDataDir
800
+ }, "Port occupied by another topic's playwright-mcp, skipping");
801
+ continue;
802
+ }
803
+ }
804
+ const pidNum = parseInt(pid, 10);
805
+ if (!Number.isNaN(pidNum)) {
806
+ killProcessTreeChildren(pidNum);
807
+ process.kill(pidNum, "SIGKILL");
808
+ }
809
+ logger.info({ pid, port }, "Killed zombie mcp-patchright");
810
+ } catch (e) {
811
+ logger.warn({ err: e, port }, "Failed to inspect process occupying port");
812
+ }
813
+ }
814
+ } catch (e) {
815
+ logger.warn({ err: e, port }, "Failed to check processes on port");
816
+ }
817
+ const start = Date.now();
818
+ while (Date.now() - start < 3000) {
819
+ if (!isPortInUse(port))
820
+ return;
821
+ await delay(200);
822
+ }
823
+ }
824
+ function browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir) {
825
+ const actualUserDataDir = extractUserDataDirArg(cmdline);
826
+ return actualUserDataDir !== null && resolve(actualUserDataDir) === resolve(expectedUserDataDir);
827
+ }
828
+ function killBrowserProcsForUserDataDir(userDataDir) {
829
+ const target = resolve(userDataDir);
830
+ if (!target.startsWith(resolve(BROWSER_PROFILES_DIR)))
831
+ return;
832
+ let pids;
833
+ try {
834
+ pids = execFileSync("pgrep", ["-f", "--", userDataDir], { stdio: "pipe" }).toString().trim();
835
+ } catch {
836
+ return;
837
+ }
838
+ if (!pids)
839
+ return;
840
+ for (const pid of pids.split(`
841
+ `)) {
842
+ const pidNum = parseInt(pid.trim(), 10);
843
+ if (Number.isNaN(pidNum) || pidNum === process.pid)
844
+ continue;
845
+ try {
846
+ const cmdline = execFileSync("ps", ["-p", String(pidNum), "-o", "command="], {
847
+ stdio: "pipe"
848
+ }).toString().trim();
849
+ const argDir = extractUserDataDirArg(cmdline);
850
+ if (!argDir || resolve(argDir) !== target)
851
+ continue;
852
+ killProcessTreeChildren(pidNum);
853
+ process.kill(pidNum, "SIGKILL");
854
+ logger.info({ pid: pidNum, userDataDir }, "Reaped orphaned browser process by user-data-dir");
855
+ } catch (e) {
856
+ logger.debug({ err: e, pid: pidNum, userDataDir }, "Failed to reap browser proc by dir");
857
+ }
858
+ }
859
+ }
860
+ function selectOrphanBrowserPids(procs, liveUserDataDirs, profileRoot, selfPid) {
861
+ const root = resolve(profileRoot);
862
+ const live = new Set([...liveUserDataDirs].map((d) => resolve(d)));
863
+ const out = [];
864
+ for (const { pid, userDataDir } of procs) {
865
+ if (pid === selfPid || !userDataDir)
866
+ continue;
867
+ const dir = resolve(userDataDir);
868
+ if (!dir.startsWith(root))
869
+ continue;
870
+ if (live.has(dir))
871
+ continue;
872
+ out.push(pid);
873
+ }
874
+ return out;
875
+ }
876
+ function reapOrphanBrowsers() {
877
+ const profileRoot = resolve(BROWSER_PROFILES_DIR);
878
+ let pids;
879
+ try {
880
+ pids = execFileSync("pgrep", ["-f", "--", profileRoot], { stdio: "pipe" }).toString().trim();
881
+ } catch {
882
+ return;
883
+ }
884
+ if (!pids)
885
+ return;
886
+ const procs = [];
887
+ for (const pid of pids.split(`
888
+ `)) {
889
+ const pidNum = parseInt(pid.trim(), 10);
890
+ if (Number.isNaN(pidNum))
891
+ continue;
892
+ try {
893
+ const cmdline = execFileSync("ps", ["-p", String(pidNum), "-o", "command="], {
894
+ stdio: "pipe"
895
+ }).toString().trim();
896
+ procs.push({ pid: pidNum, userDataDir: extractUserDataDirArg(cmdline) });
897
+ } catch {}
898
+ }
899
+ const liveDirs = [...instances.keys()].map(resolveUserDataDir);
900
+ const orphanPids = selectOrphanBrowserPids(procs, liveDirs, profileRoot, process.pid);
901
+ for (const pid of orphanPids) {
902
+ try {
903
+ killProcessTreeChildren(pid);
904
+ process.kill(pid, "SIGKILL");
905
+ logger.info({ pid }, "Reaped orphaned browser process (janitor)");
906
+ } catch (e) {
907
+ logger.debug({ err: e, pid }, "Janitor failed to reap orphaned browser process");
908
+ }
909
+ }
910
+ }
911
+ async function isHealthy(port) {
912
+ for (const path of ["/health", "/sse?owner=__negotium_health__"]) {
913
+ try {
914
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
915
+ signal: AbortSignal.timeout(2000)
916
+ });
917
+ await res.body?.cancel();
918
+ if (res.ok)
919
+ return true;
920
+ } catch {}
921
+ }
922
+ return false;
923
+ }
924
+ function cleanSingletonFiles(userDataDir) {
925
+ try {
926
+ const files = readdirSync(userDataDir);
927
+ for (const f of files) {
928
+ if (f.startsWith("Singleton")) {
929
+ try {
930
+ unlinkSync(resolve(userDataDir, f));
931
+ logger.info({ file: f, userDataDir }, "Removed stale Singleton file");
932
+ } catch (e) {
933
+ logger.warn({ err: e, file: f }, "Failed to remove stale Chrome Singleton file");
934
+ }
935
+ }
936
+ }
937
+ } catch {}
938
+ }
939
+ function killProcessTreeChildren(pid) {
940
+ try {
941
+ const children = execFileSync("pgrep", ["-P", String(pid)], { stdio: "pipe" }).toString().trim();
942
+ if (!children)
943
+ return;
944
+ const childPids = children.split(`
945
+ `).map((cpid) => parseInt(cpid, 10)).filter((pidNum) => !Number.isNaN(pidNum));
946
+ for (const childPid of childPids) {
947
+ killProcessTreeChildren(childPid);
948
+ }
949
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
950
+ for (const childPid of childPids) {
951
+ try {
952
+ process.kill(childPid, signal);
953
+ } catch (e) {
954
+ logger.debug({ err: e, pid: childPid, signal }, "Failed to signal browser child process");
955
+ }
956
+ }
957
+ }
958
+ logger.info({ parentPid: pid, childPids }, "Killed browser-MCP child process tree");
959
+ } catch {}
960
+ }
961
+ function ownerDirectory(ownerId) {
962
+ const digest = createHash("sha256").update(ownerId).digest("hex").slice(0, 16);
963
+ return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
964
+ }
965
+ function resolveProfileDir(ownerId, profile) {
966
+ return resolve(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
967
+ }
968
+ function resolveUserDataDir(instanceKey) {
969
+ const { ownerId, profile } = parseInstanceKey(instanceKey);
970
+ return resolveProfileDir(ownerId, profile);
971
+ }
972
+ function killInstance(instanceKey, opts) {
973
+ const inst = instances.get(instanceKey);
974
+ if (!inst)
975
+ return;
976
+ if (inst.process.pid) {
977
+ killProcessTreeChildren(inst.process.pid);
978
+ }
979
+ try {
980
+ inst.process.kill("SIGTERM");
981
+ } catch (e) {
982
+ logger.warn({ err: e, instanceKey }, "Failed to kill playwright instance");
983
+ }
984
+ inst.process.removeAllListeners("error");
985
+ inst.process.removeAllListeners("exit");
986
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
987
+ if (!opts?.keepPort)
988
+ releasePort(inst.port);
989
+ instances.delete(instanceKey);
990
+ deletePortFile(instanceKey);
991
+ logger.info({ instanceKey, port: inst.port, keepPort: !!opts?.keepPort }, "Killed Playwright MCP (with cleanup)");
992
+ }
993
+ async function spawnPlaywright(instanceKey, userId, reservedPort, browserBin = PLAYWRIGHT_MCP_BIN, allowFallback = true) {
994
+ const userDataDir = resolveUserDataDir(instanceKey);
995
+ const port = reservedPort ?? await allocatePort(userDataDir);
996
+ mkdirSync(userDataDir, { recursive: true });
997
+ const mcpArgs = [
998
+ "--port",
999
+ String(port),
1000
+ "--host",
1001
+ "127.0.0.1",
1002
+ "--user-data-dir",
1003
+ userDataDir
1004
+ ];
1005
+ const proxy = resolveBrowserProxy();
1006
+ const capability = randomBytes(32).toString("hex");
1007
+ const childEnv = {
1008
+ ...process.env,
1009
+ NEGOTIUM_BROWSER_CAPABILITY: capability,
1010
+ ...proxy ? {
1011
+ NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
1012
+ ...proxy.username ? { NEGOTIUM_BROWSER_PROXY_USERNAME: proxy.username } : {},
1013
+ ...proxy.password ? { NEGOTIUM_BROWSER_PROXY_PASSWORD: proxy.password } : {},
1014
+ ...proxy.bypass ? { NEGOTIUM_BROWSER_PROXY_BYPASS: proxy.bypass } : {}
1015
+ } : {}
1016
+ };
1017
+ if (proxy) {
1018
+ logger.info({ instanceKey, proxyServer: proxy.server }, "Browser egress proxy enabled");
1019
+ }
1020
+ const command = browserBin.endsWith(".mjs") ? process.execPath : browserBin;
1021
+ const args = browserBin.endsWith(".mjs") ? [browserBin, ...mcpArgs] : mcpArgs;
1022
+ const proc = spawn(command, args, {
1023
+ stdio: "ignore",
1024
+ detached: false,
1025
+ env: childEnv
1026
+ });
1027
+ const reapCrashedBrowser = () => {
1028
+ const userDataDir2 = resolveUserDataDir(instanceKey);
1029
+ killBrowserProcsForUserDataDir(userDataDir2);
1030
+ cleanSingletonFiles(userDataDir2);
1031
+ };
1032
+ proc.once("error", (err) => {
1033
+ logger.error({ err, instanceKey }, "Playwright MCP error");
1034
+ if (instances.get(instanceKey)?.process === proc) {
1035
+ releasePort(port);
1036
+ instances.delete(instanceKey);
1037
+ reapCrashedBrowser();
1038
+ }
1039
+ });
1040
+ proc.once("exit", (code) => {
1041
+ logger.info({ instanceKey, code }, "Playwright MCP exited");
1042
+ const wasOurs = instances.get(instanceKey)?.process === proc;
1043
+ if (wasOurs) {
1044
+ releasePort(port);
1045
+ instances.delete(instanceKey);
1046
+ reapCrashedBrowser();
1047
+ _onPlaywrightExit?.(instanceKey, code);
1048
+ }
1049
+ });
1050
+ const now = Date.now();
1051
+ instances.set(instanceKey, {
1052
+ process: proc,
1053
+ port,
1054
+ userId,
1055
+ startedAt: now,
1056
+ lastUsedAt: now,
1057
+ capability
1058
+ });
1059
+ const ready = await waitForServer(port, 1e4) && await supportsOwnerCleanup(port, capability);
1060
+ if (!ready) {
1061
+ const exitCode = proc.exitCode;
1062
+ killInstance(instanceKey);
1063
+ if (allowFallback && browserBin !== PATCHRIGHT_MCP_BIN) {
1064
+ logger.warn({ instanceKey, browserBin, fallback: PATCHRIGHT_MCP_BIN }, "Preferred browser MCP unavailable or lacks owner isolation; using Patchright fallback");
1065
+ return spawnPlaywright(instanceKey, userId, undefined, PATCHRIGHT_MCP_BIN, false);
1066
+ }
1067
+ throw new Error(`Playwright MCP failed health check after spawn on port ${port}` + (exitCode === null ? "" : ` (exitCode=${exitCode})`));
1068
+ }
1069
+ writePortFile(instanceKey, port);
1070
+ logger.info({ instanceKey, port, pid: proc.pid, ready }, "Playwright MCP started");
1071
+ return port;
1072
+ }
1073
+ async function supportsOwnerCleanup(port, capability) {
1074
+ try {
1075
+ const response = await fetch(`http://127.0.0.1:${port}/owners`, {
1076
+ method: "DELETE",
1077
+ headers: {
1078
+ "X-Browser-Owner": "__negotium_capability_probe__",
1079
+ "X-Browser-Capability": capability
1080
+ },
1081
+ signal: AbortSignal.timeout(2000)
1082
+ });
1083
+ await response.body?.cancel();
1084
+ return response.ok;
1085
+ } catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ async function ensurePlaywright(userId, topic) {
1090
+ const instanceKey = makeInstanceKey(userId, topic);
1091
+ const inProgress = spawning.get(instanceKey);
1092
+ if (inProgress) {
1093
+ const port = await inProgress;
1094
+ if (port !== null)
1095
+ return port;
1096
+ return ensurePlaywright(userId, topic);
1097
+ }
1098
+ const promise = (async () => {
1099
+ const existing = instances.get(instanceKey);
1100
+ if (existing && !existing.process.killed && existing.process.exitCode === null) {
1101
+ if (await isHealthy(existing.port)) {
1102
+ existing.lastUsedAt = Date.now();
1103
+ return existing.port;
1104
+ }
1105
+ logger.warn({ instanceKey }, "Playwright MCP unresponsive, restarting");
1106
+ const oldPort = existing.port;
1107
+ killInstance(instanceKey);
1108
+ await waitForPortRelease(oldPort);
1109
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1110
+ } else if (existing) {
1111
+ releasePort(existing.port);
1112
+ instances.delete(instanceKey);
1113
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1114
+ }
1115
+ return spawnPlaywright(instanceKey, userId);
1116
+ })().finally(() => spawning.delete(instanceKey));
1117
+ spawning.set(instanceKey, promise);
1118
+ return promise;
1119
+ }
1120
+ async function waitForPortRelease(port, timeoutMs = 3000) {
1121
+ const start = Date.now();
1122
+ while (Date.now() - start < timeoutMs) {
1123
+ if (!isPortInUse(port))
1124
+ return;
1125
+ await delay(200);
1126
+ }
1127
+ }
1128
+ async function closeBrowserOwnerTabs(ownerId, rawProfile, owner) {
1129
+ const instanceKey = makeBrowserProfileInstanceKey(ownerId, rawProfile);
1130
+ const inProgress = spawning.get(instanceKey);
1131
+ if (inProgress)
1132
+ await inProgress;
1133
+ const instance = instances.get(instanceKey);
1134
+ if (!instance)
1135
+ return 0;
1136
+ const port = instance.port;
1137
+ const response = await fetch(`http://127.0.0.1:${port}/owners`, {
1138
+ method: "DELETE",
1139
+ headers: {
1140
+ "X-Browser-Owner": owner,
1141
+ "X-Browser-Capability": instance.capability
1142
+ },
1143
+ signal: AbortSignal.timeout(5000)
1144
+ });
1145
+ if (!response.ok)
1146
+ throw new Error(`browser MCP owner cleanup failed (${response.status})`);
1147
+ const result = await response.json();
1148
+ return typeof result.closed === "number" ? result.closed : 0;
1149
+ }
1150
+ async function waitForServer(port, timeoutMs) {
1151
+ const start = Date.now();
1152
+ while (Date.now() - start < timeoutMs) {
1153
+ if (await isHealthy(port))
1154
+ return true;
1155
+ await delay(300);
1156
+ }
1157
+ logger.warn({ port, timeoutMs }, "Playwright MCP not ready before timeout");
1158
+ return false;
1159
+ }
1160
+ async function cloneProfileForChild(opts) {
1161
+ const srcOwner = getBrowserProfileOwner(opts.srcTopic, opts.userId);
1162
+ const dstOwner = getBrowserProfileOwner(opts.dstTopic, opts.userId);
1163
+ if (srcOwner !== dstOwner) {
1164
+ const dstDir2 = resolveProfileDir(dstOwner, getTopicBrowserProfile(opts.dstTopic));
1165
+ return {
1166
+ copied: false,
1167
+ srcDir: resolveProfileDir(srcOwner, getTopicBrowserProfile(opts.srcTopic)),
1168
+ dstDir: dstDir2,
1169
+ reason: "cross-owner-fresh-profile"
1170
+ };
1171
+ }
1172
+ if (srcOwner === dstOwner && hasBrowserProfileTopic(opts.dstTopic)) {
1173
+ const profile = getTopicBrowserProfile(opts.srcTopic);
1174
+ assignTopicBrowserProfile({ topicId: opts.dstTopic, actorUserId: dstOwner, profile });
1175
+ const sharedDir = resolveProfileDir(srcOwner, profile);
1176
+ return {
1177
+ copied: false,
1178
+ srcDir: sharedDir,
1179
+ dstDir: sharedDir,
1180
+ reason: "shared-profile-assignment"
1181
+ };
1182
+ }
1183
+ const srcKey = makeInstanceKey(opts.userId, opts.srcTopic);
1184
+ const dstKey = makeInstanceKey(opts.userId, opts.dstTopic);
1185
+ const srcDir = resolveUserDataDir(srcKey);
1186
+ const dstDir = resolveUserDataDir(dstKey);
1187
+ if (srcDir === dstDir) {
1188
+ return { copied: false, srcDir, dstDir, reason: "same-dir" };
1189
+ }
1190
+ return withPlaywrightInstanceMaintenance([srcKey, dstKey], async () => {
1191
+ const parentWasLive = instances.has(srcKey);
1192
+ if (parentWasLive) {
1193
+ logger.info({ srcKey }, "Quiescing parent Playwright before profile clone");
1194
+ killInstance(srcKey);
1195
+ await delay(1500);
1196
+ }
1197
+ if (instances.has(dstKey)) {
1198
+ killInstance(dstKey);
1199
+ await delay(500);
1200
+ }
1201
+ if (!existsSync(srcDir)) {
1202
+ return { copied: false, srcDir, dstDir, reason: "src-missing" };
1203
+ }
1204
+ try {
1205
+ if (existsSync(dstDir)) {
1206
+ rmSync(dstDir, { recursive: true, force: true });
1207
+ }
1208
+ mkdirSync(dirname(dstDir), { recursive: true });
1209
+ if (process.platform === "darwin") {
1210
+ try {
1211
+ execFileSync("cp", ["-cR", srcDir, dstDir], { stdio: "pipe" });
1212
+ } catch {
1213
+ cpSync(srcDir, dstDir, { recursive: true });
1214
+ }
1215
+ } else {
1216
+ cpSync(srcDir, dstDir, { recursive: true });
1217
+ }
1218
+ } catch (e) {
1219
+ const reason = `copy-failed: ${e instanceof Error ? e.message : String(e)}`;
1220
+ logger.warn({ srcDir, dstDir, err: e }, "Playwright profile clone failed");
1221
+ return { copied: false, srcDir, dstDir, reason };
1222
+ }
1223
+ cleanSingletonFiles(dstDir);
1224
+ for (const f of ["DevToolsActivePort", "LOCK"]) {
1225
+ try {
1226
+ unlinkSync(resolve(dstDir, f));
1227
+ } catch {}
1228
+ }
1229
+ logger.info({ srcKey, dstKey, srcDir, dstDir }, "Cloned Playwright profile for child topic");
1230
+ return { copied: true, srcDir, dstDir };
1231
+ });
1232
+ }
1233
+ function deleteTopicProfileDir(userId, topic) {
1234
+ const key = makeInstanceKey(userId, topic);
1235
+ const dir = resolveUserDataDir(key);
1236
+ logger.info({ dir, userId, topic }, "Preserved shared browser profile on topic deletion");
1237
+ return { deleted: false, dir };
1238
+ }
1239
+ async function killAllPlaywright() {
1240
+ const procs = [...instances.entries()].map(([key, inst]) => ({ key, proc: inst.process }));
1241
+ for (const { key } of procs)
1242
+ killInstance(key);
1243
+ await Promise.all(procs.map(async ({ key, proc }) => {
1244
+ if (await waitForChildProcessExit(proc, 3000))
1245
+ return;
1246
+ logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP ignored SIGTERM");
1247
+ try {
1248
+ proc.kill("SIGKILL");
1249
+ } catch (err) {
1250
+ logger.warn({ err, instanceKey: key, pid: proc.pid }, "Failed to SIGKILL Playwright MCP");
1251
+ }
1252
+ if (!await waitForChildProcessExit(proc, 1000)) {
1253
+ logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP did not report exit");
1254
+ }
1255
+ }));
1256
+ }
1257
+ function waitForChildProcessExit(proc, timeoutMs) {
1258
+ if (proc.exitCode !== null || proc.signalCode !== null)
1259
+ return Promise.resolve(true);
1260
+ return new Promise((resolveWait) => {
1261
+ let settled = false;
1262
+ let timer;
1263
+ const finish = (exited) => {
1264
+ if (settled)
1265
+ return;
1266
+ settled = true;
1267
+ clearTimeout(timer);
1268
+ proc.off("exit", onExit);
1269
+ resolveWait(exited);
1270
+ };
1271
+ const onExit = () => finish(true);
1272
+ proc.once("exit", onExit);
1273
+ timer = setTimeout(() => finish(proc.exitCode !== null || proc.signalCode !== null), timeoutMs);
1274
+ if (typeof timer === "object")
1275
+ timer.unref?.();
1276
+ if (proc.exitCode !== null || proc.signalCode !== null)
1277
+ finish(true);
1278
+ });
1279
+ }
1280
+ setInterval(() => {
1281
+ while (evictIdleInstance() !== null) {}
1282
+ try {
1283
+ reapOrphanBrowsers();
1284
+ } catch (e) {
1285
+ logger.debug({ err: e }, "reapOrphanBrowsers sweep failed");
1286
+ }
1287
+ }, 30 * 60 * 1000).unref();
1288
+
1289
+ // ../../packages/core/src/topics/personal-general.ts
1290
+ import { randomUUID as randomUUID2 } from "crypto";
1291
+ var LEGACY_PERSONAL_GENERAL_DESCRIPTION = "\uB098\uB9CC\uC758 \uAC1C\uC778 \uACF5\uAC04\uC774\uC5D0\uC694. \uB300\uD654\uC640 AI \uC791\uC5C5\uC740 \uB2E4\uB978 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uAC1C\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.";
1292
+ var PERSONAL_GENERAL_DESCRIPTION = "Your private General. Messages and membership are visible only to you. Workspace memory, wiki, and skills are shared with your workspace.";
1293
+ function ensurePersonalGeneral(userId) {
1294
+ const existing = getManagerTopicForUser(userId);
1295
+ if (existing) {
1296
+ if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
1297
+ existing.description = PERSONAL_GENERAL_DESCRIPTION;
1298
+ upsertTopic(existing);
1299
+ }
1300
+ if (!getApiTopicConfig(existing.id)) {
1301
+ setApiTopicConfig(existing.id, { model: "deepseek-pro", modelLocked: true });
1302
+ }
1303
+ return existing;
1304
+ }
1305
+ const now = new Date().toISOString();
1306
+ const registry = getRegistry("maestro");
1307
+ const topic = {
1308
+ id: randomUUID2(),
1309
+ title: "General",
1310
+ description: PERSONAL_GENERAL_DESCRIPTION,
1311
+ kind: "manager",
1312
+ agent: "maestro",
1313
+ defaultModel: resolveDefaultModel("maestro", registry.defaultModel),
1314
+ defaultEffort: registry.defaultEffort ?? "medium",
1315
+ aiMode: "always",
1316
+ aiMention: false,
1317
+ participants: [{ userId, role: "owner" }],
1318
+ createdAt: now,
1319
+ lastMessageAt: now
1320
+ };
1321
+ upsertTopic(topic);
1322
+ setApiTopicConfig(topic.id, { model: "deepseek-pro", modelLocked: true });
1323
+ return topic;
1324
+ }
1325
+ function isLegacySharedGeneral(topicId) {
1326
+ return topicId === GENERAL_TOPIC_ID;
1327
+ }
1328
+
1329
+ export { latestRuntimeEventSeq, listRuntimeEventsAfter, listRecentRuntimeEventsForTopic, runtimeBus, setRuntimeBus, WsHub, appendApiMessage, getApiMessage, topicHasAttachmentFileId, updateApiMessageText, updateApiMessageUsage, updateApiMessageAskUserQuestion, updateApiMessageSubagentCard, listApiMessagesByKind, softDeleteApiMessage, listApiMessages, deleteMessagesForTopic, copyMessagesForTopic, getAllMessagesForTopic, getMessagesForTopicAfterRowid, delay, getBrowserProfileOwner, isTopicBrowserProfileOwner, getTopicBrowserProfile, makeInstanceKey, pinPlaywrightInstance, unpinPlaywrightInstance, getPlaywrightCapability, ensurePlaywright, closeBrowserOwnerTabs, cloneProfileForChild, deleteTopicProfileDir, killAllPlaywright, ensurePersonalGeneral, isLegacySharedGeneral };
1330
+
1331
+ //# debugId=FD285E5E50ECD13364756E2164756E21