negotium 0.1.7 → 0.1.9

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 (270) hide show
  1. package/README.md +26 -2
  2. package/dist/canonical-mcp-bridge.js +14 -0
  3. package/dist/canonical-mcp-bridge.js.map +9 -0
  4. package/dist/chunk-10ct8814.js +429 -0
  5. package/dist/chunk-10ct8814.js.map +11 -0
  6. package/dist/chunk-18fzftjf.js +214 -0
  7. package/dist/chunk-18fzftjf.js.map +10 -0
  8. package/dist/chunk-1k7t7bnx.js +97 -0
  9. package/dist/chunk-1k7t7bnx.js.map +10 -0
  10. package/dist/chunk-1msje2zf.js +23 -0
  11. package/dist/chunk-1msje2zf.js.map +10 -0
  12. package/dist/chunk-2dgkah1z.js +706 -0
  13. package/dist/chunk-2dgkah1z.js.map +14 -0
  14. package/dist/chunk-3t4dtm26.js +61 -0
  15. package/dist/chunk-3t4dtm26.js.map +10 -0
  16. package/dist/chunk-3vn2vnj1.js +51 -0
  17. package/dist/chunk-3vn2vnj1.js.map +10 -0
  18. package/dist/chunk-6hknphtc.js +2054 -0
  19. package/dist/chunk-6hknphtc.js.map +29 -0
  20. package/dist/chunk-6tfzpcvk.js +2149 -0
  21. package/dist/chunk-6tfzpcvk.js.map +15 -0
  22. package/dist/chunk-8g0c4as0.js +23 -0
  23. package/dist/chunk-8g0c4as0.js.map +10 -0
  24. package/dist/chunk-9spx3h9n.js +279 -0
  25. package/dist/chunk-9spx3h9n.js.map +10 -0
  26. package/dist/chunk-c500xe9z.js +33 -0
  27. package/dist/chunk-c500xe9z.js.map +10 -0
  28. package/dist/chunk-e9b2xpff.js +147 -0
  29. package/dist/chunk-e9b2xpff.js.map +10 -0
  30. package/dist/chunk-edb07sjp.js +567 -0
  31. package/dist/chunk-edb07sjp.js.map +14 -0
  32. package/dist/chunk-eexqsee7.js +368 -0
  33. package/dist/chunk-eexqsee7.js.map +11 -0
  34. package/dist/chunk-efyxk97v.js +500 -0
  35. package/dist/chunk-efyxk97v.js.map +13 -0
  36. package/dist/chunk-epqxwkvh.js +3534 -0
  37. package/dist/chunk-epqxwkvh.js.map +20 -0
  38. package/dist/chunk-f1pzh3xh.js +115 -0
  39. package/dist/chunk-f1pzh3xh.js.map +10 -0
  40. package/dist/chunk-f5xgprg1.js +59 -0
  41. package/dist/chunk-f5xgprg1.js.map +10 -0
  42. package/dist/chunk-f97qybwk.js +584 -0
  43. package/dist/chunk-f97qybwk.js.map +11 -0
  44. package/dist/chunk-fvc7ncxd.js +252 -0
  45. package/dist/chunk-fvc7ncxd.js.map +10 -0
  46. package/dist/chunk-g7pvfj5e.js +574 -0
  47. package/dist/chunk-g7pvfj5e.js.map +14 -0
  48. package/dist/chunk-h8vmb9gf.js +315 -0
  49. package/dist/chunk-h8vmb9gf.js.map +10 -0
  50. package/dist/chunk-hgw7n8cm.js +43 -0
  51. package/dist/chunk-hgw7n8cm.js.map +10 -0
  52. package/dist/chunk-hsq7j9jk.js +126 -0
  53. package/dist/chunk-hsq7j9jk.js.map +10 -0
  54. package/dist/chunk-hwy3n31w.js +21 -0
  55. package/dist/chunk-hwy3n31w.js.map +9 -0
  56. package/dist/chunk-j0kjscb7.js +53 -0
  57. package/dist/chunk-j0kjscb7.js.map +10 -0
  58. package/dist/chunk-jzz91jwh.js +70 -0
  59. package/dist/chunk-jzz91jwh.js.map +10 -0
  60. package/dist/chunk-k9z47mab.js +247 -0
  61. package/dist/chunk-k9z47mab.js.map +12 -0
  62. package/dist/chunk-kbm35t7w.js +1331 -0
  63. package/dist/chunk-kbm35t7w.js.map +16 -0
  64. package/dist/chunk-m4veth0c.js +1649 -0
  65. package/dist/chunk-m4veth0c.js.map +17 -0
  66. package/dist/chunk-mhty3nna.js +150 -0
  67. package/dist/chunk-mhty3nna.js.map +11 -0
  68. package/dist/chunk-mmgh670x.js +248 -0
  69. package/dist/chunk-mmgh670x.js.map +11 -0
  70. package/dist/chunk-n0kk5xbq.js +2358 -0
  71. package/dist/chunk-n0kk5xbq.js.map +26 -0
  72. package/dist/chunk-nmv41qx9.js +2847 -0
  73. package/dist/chunk-nmv41qx9.js.map +23 -0
  74. package/dist/chunk-nqdp52aj.js +113 -0
  75. package/dist/chunk-nqdp52aj.js.map +12 -0
  76. package/dist/chunk-ns2m25dz.js +139 -0
  77. package/dist/chunk-ns2m25dz.js.map +10 -0
  78. package/dist/chunk-pkdmypdd.js +147 -0
  79. package/dist/chunk-pkdmypdd.js.map +11 -0
  80. package/dist/chunk-rmyfmrrr.js +1634 -0
  81. package/dist/chunk-rmyfmrrr.js.map +15 -0
  82. package/dist/chunk-sa874j5c.js +9 -0
  83. package/dist/chunk-sa874j5c.js.map +10 -0
  84. package/dist/chunk-sgsz2fpq.js +289 -0
  85. package/dist/chunk-sgsz2fpq.js.map +10 -0
  86. package/dist/chunk-sn51wnf1.js +3020 -0
  87. package/dist/chunk-sn51wnf1.js.map +32 -0
  88. package/dist/chunk-sqhr1a7e.js +587 -0
  89. package/dist/chunk-sqhr1a7e.js.map +11 -0
  90. package/dist/chunk-tmj5r2zv.js +2933 -0
  91. package/dist/chunk-tmj5r2zv.js.map +20 -0
  92. package/dist/chunk-vefj3880.js +56 -0
  93. package/dist/chunk-vefj3880.js.map +10 -0
  94. package/dist/chunk-vp2db42d.js +121 -0
  95. package/dist/chunk-vp2db42d.js.map +10 -0
  96. package/dist/chunk-wry79q55.js +108 -0
  97. package/dist/chunk-wry79q55.js.map +10 -0
  98. package/dist/chunk-x1arr7ze.js +63 -0
  99. package/dist/chunk-x1arr7ze.js.map +10 -0
  100. package/dist/chunk-z2xy5n2q.js +103 -0
  101. package/dist/chunk-z2xy5n2q.js.map +10 -0
  102. package/dist/hosted-agent.js +53 -0
  103. package/dist/hosted-agent.js.map +10 -0
  104. package/dist/main.js +132 -20
  105. package/dist/main.js.map +4 -4
  106. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  107. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  108. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  109. package/dist/runtime/src/agents/archiver.ts +396 -0
  110. package/dist/runtime/src/agents/auth-check.ts +64 -0
  111. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  112. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  113. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  114. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  115. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  116. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  117. package/dist/runtime/src/agents/contracts.ts +69 -0
  118. package/dist/runtime/src/agents/deep-map.ts +13 -0
  119. package/dist/runtime/src/agents/execution-host.ts +91 -0
  120. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  121. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  122. package/dist/runtime/src/agents/fork.ts +65 -0
  123. package/dist/runtime/src/agents/hosted-agent.ts +49 -0
  124. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  125. package/dist/runtime/src/agents/index.ts +205 -0
  126. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  127. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  128. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  129. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  130. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  131. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  132. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  133. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  134. package/dist/runtime/src/agents/registry.ts +24 -0
  135. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  136. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  137. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  138. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  139. package/dist/runtime/src/agents/task-events.ts +44 -0
  140. package/dist/runtime/src/agents/tool-format.ts +352 -0
  141. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  142. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  143. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  144. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  145. package/dist/runtime/src/application/submit-user-message.ts +50 -0
  146. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  147. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  148. package/dist/runtime/src/application/topic-service.ts +139 -0
  149. package/dist/runtime/src/application/vault-command.ts +170 -0
  150. package/dist/runtime/src/bus.ts +319 -0
  151. package/dist/runtime/src/index.ts +333 -0
  152. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  153. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  154. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  155. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  156. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  157. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  158. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  159. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  160. package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
  161. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  162. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  163. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  164. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  165. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  166. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  167. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  168. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  169. package/dist/runtime/src/mcp/task-server.ts +173 -0
  170. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  171. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  172. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  173. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  174. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  175. package/dist/runtime/src/media/file-events.ts +33 -0
  176. package/dist/runtime/src/media/text-extractor.ts +312 -0
  177. package/dist/runtime/src/media/video.ts +92 -0
  178. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  179. package/dist/runtime/src/outbox/index.ts +8 -0
  180. package/dist/runtime/src/outbox/utils.ts +51 -0
  181. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  182. package/dist/runtime/src/platform/config.ts +371 -0
  183. package/dist/runtime/src/platform/constants.ts +18 -0
  184. package/dist/runtime/src/platform/debug-users.ts +14 -0
  185. package/dist/runtime/src/platform/delay.ts +1 -0
  186. package/dist/runtime/src/platform/error.ts +9 -0
  187. package/dist/runtime/src/platform/file-utils.ts +16 -0
  188. package/dist/runtime/src/platform/jsonl.ts +202 -0
  189. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  190. package/dist/runtime/src/platform/logger.ts +31 -0
  191. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  192. package/dist/runtime/src/platform/mcp-config.ts +878 -0
  193. package/dist/runtime/src/platform/modules.ts +145 -0
  194. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  195. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  196. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  197. package/dist/runtime/src/prompts/builders.ts +368 -0
  198. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  199. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  200. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  201. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  202. package/dist/runtime/src/query/active-rooms.ts +534 -0
  203. package/dist/runtime/src/query/control.ts +313 -0
  204. package/dist/runtime/src/query/index.ts +24 -0
  205. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  206. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  207. package/dist/runtime/src/query/state.ts +94 -0
  208. package/dist/runtime/src/query/types.ts +83 -0
  209. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  210. package/dist/runtime/src/runtime/attachments.ts +151 -0
  211. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  212. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  213. package/dist/runtime/src/runtime/errors.ts +69 -0
  214. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  215. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  216. package/dist/runtime/src/runtime/footer.ts +23 -0
  217. package/dist/runtime/src/runtime/inbox.ts +946 -0
  218. package/dist/runtime/src/runtime/tasks.ts +56 -0
  219. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  220. package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
  221. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  222. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  223. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  224. package/dist/runtime/src/runtime/visuals.ts +171 -0
  225. package/dist/runtime/src/security/sanitize.ts +27 -0
  226. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  227. package/dist/runtime/src/storage/activity-log.ts +152 -0
  228. package/dist/runtime/src/storage/api-messages.ts +617 -0
  229. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  230. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  231. package/dist/runtime/src/storage/api-topics.ts +844 -0
  232. package/dist/runtime/src/storage/app-settings.ts +44 -0
  233. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  234. package/dist/runtime/src/storage/conversations.ts +268 -0
  235. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  236. package/dist/runtime/src/storage/forum/index.ts +4 -0
  237. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  238. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  239. package/dist/runtime/src/storage/forum-db.ts +23 -0
  240. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  241. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  242. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  243. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  244. package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
  245. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  246. package/dist/runtime/src/storage/session-asks.ts +347 -0
  247. package/dist/runtime/src/storage/sqlite.ts +129 -0
  248. package/dist/runtime/src/storage/tasks.ts +179 -0
  249. package/dist/runtime/src/storage/token-stats.ts +144 -0
  250. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  251. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  252. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  253. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  254. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  255. package/dist/runtime/src/storage/vault.ts +225 -0
  256. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  257. package/dist/runtime/src/storage/wiki.ts +85 -0
  258. package/dist/runtime/src/topics/create.ts +123 -0
  259. package/dist/runtime/src/topics/derive.ts +398 -0
  260. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  261. package/dist/runtime/src/topics/links.ts +7 -0
  262. package/dist/runtime/src/topics/personal-general.ts +65 -0
  263. package/dist/runtime/src/topics/session.ts +381 -0
  264. package/dist/runtime/src/types/api.ts +497 -0
  265. package/dist/runtime/src/types.ts +257 -0
  266. package/dist/runtime/src/version.ts +1 -0
  267. package/dist/runtime/tsconfig.json +13 -0
  268. package/package.json +28 -3
  269. package/dist/main.d.ts +0 -3
  270. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Derived-topic creation — fork (config+history copy), spawn (config-only),
3
+ * and subagent (agent-initiated worker room) share this helper.
4
+ *
5
+ * Ported from otium runtime-api `api/routes/topics.ts`, minus the REST route
6
+ * table; the route-facing wrappers (`getTopics`, `updateTopic`) live here too
7
+ * so ported call sites keep working.
8
+ */
9
+
10
+ import { randomUUID } from "node:crypto";
11
+ import { mkdirSync } from "node:fs";
12
+ import { cleanupAgentFork, type ForkHandle, forkAgentSession } from "#agents/fork";
13
+ import { resolveModelForAgent } from "#agents/model-catalog";
14
+ import { getRegistry } from "#agents/registry";
15
+ import { WsHub } from "#bus";
16
+ import { resolveTopicWorkspaceDir } from "#platform/config";
17
+ import { logger } from "#platform/logger";
18
+ import { cloneProfileForChild } from "#platform/playwright/manager";
19
+ import { isTopicRunning } from "#query/active-rooms";
20
+ import { copyMessagesForTopic } from "#storage/api-messages";
21
+ import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
22
+ import {
23
+ findTopicTitleConflict,
24
+ getTopic,
25
+ getTopicSessionId,
26
+ inferTopicKind,
27
+ isTopicVisible,
28
+ listTopics,
29
+ setTopicSessionId,
30
+ upsertTopic,
31
+ } from "#storage/api-topics";
32
+ import {
33
+ appendConversationEventStrict,
34
+ cloneConversationLog,
35
+ readConversation,
36
+ } from "#storage/conversations";
37
+ import { db } from "#storage/forum-db";
38
+ import { beginRuntimeTopicMaintenance } from "#storage/runtime-topic-state";
39
+ import { isLegacySharedGeneral } from "#topics/personal-general";
40
+ import type { AgentKind } from "#types";
41
+ import type { TopicDto } from "#types/api";
42
+
43
+ export function getTopics(): TopicDto[] {
44
+ return listTopics().filter((topic) => !isLegacySharedGeneral(topic.id));
45
+ }
46
+
47
+ /** Topics adapters may show in lists and selection UIs. */
48
+ export function getVisibleTopics(): TopicDto[] {
49
+ return getTopics()
50
+ .filter(isTopicVisible)
51
+ .map((topic) => {
52
+ if (!topic.agent) return topic;
53
+ const registry = getRegistry(topic.agent);
54
+ const config = getApiTopicConfig(topic.id);
55
+ const requestedEffort = config?.effort ?? topic.defaultEffort;
56
+ return {
57
+ ...topic,
58
+ effectiveModel: resolveModelForAgent(
59
+ topic.agent,
60
+ config?.model ?? topic.defaultModel,
61
+ registry,
62
+ ),
63
+ effectiveEffort:
64
+ requestedEffort && registry.validateEffort(requestedEffort)
65
+ ? requestedEffort
66
+ : registry.defaultEffort,
67
+ };
68
+ });
69
+ }
70
+
71
+ export function updateTopic(topicId: string, patch: Partial<TopicDto>): boolean {
72
+ const topic = getTopic(topicId);
73
+ if (!topic) return false;
74
+ Object.assign(topic, patch);
75
+ upsertTopic(topic);
76
+ return true;
77
+ }
78
+
79
+ export function isParticipant(topic: TopicDto, userId: string): boolean {
80
+ return topic.participants.some((p) => p.userId === userId);
81
+ }
82
+
83
+ function nextDerivedTopicTitle(
84
+ sourceTitle: string,
85
+ kind: TopicDto["kind"],
86
+ suffix: "fork" | "spawn" | "agent",
87
+ ): string {
88
+ const visibleTitles = new Set(
89
+ listTopics()
90
+ .filter((topic) => topic.kind === kind)
91
+ .map((topic) => topic.title.toLowerCase()),
92
+ );
93
+ let n = 1;
94
+ let title = `${sourceTitle}-${suffix}-${n}`;
95
+ while (visibleTitles.has(title.toLowerCase())) {
96
+ n += 1;
97
+ title = `${sourceTitle}-${suffix}-${n}`;
98
+ }
99
+ return title;
100
+ }
101
+
102
+ function effectiveAgentForTopic(_topicId: string, topic: TopicDto): AgentKind | undefined {
103
+ if (!topic.agent) return undefined;
104
+ return topic.agent;
105
+ }
106
+
107
+ function rollbackHandleFor(agent: AgentKind, sessionId: string, rolloutPath: string): ForkHandle {
108
+ return { agent, forkId: sessionId, rolloutPath };
109
+ }
110
+
111
+ /** Thrown when a user-supplied derived-topic name collides with an existing
112
+ * topic title. Callers surface this as a specific "pick a different name"
113
+ * message instead of the generic membership/session failure. */
114
+ export class TopicTitleConflictError extends Error {
115
+ constructor(readonly title: string) {
116
+ super(`A topic named "${title}" already exists`);
117
+ this.name = "TopicTitleConflictError";
118
+ }
119
+ }
120
+
121
+ export class TopicDeriveBusyError extends Error {
122
+ constructor(message = "Topic is busy; try again when the current operation finishes") {
123
+ super(message);
124
+ this.name = "TopicDeriveBusyError";
125
+ }
126
+ }
127
+
128
+ interface DerivedTopicOptions {
129
+ name?: string;
130
+ subagent?: { agent?: AgentKind; model?: string };
131
+ allowActiveSource?: boolean;
132
+ }
133
+
134
+ /**
135
+ * Shared helper for spawn (config-only copy) and fork (config+history copy).
136
+ *
137
+ * - fork (copyHistory=true): inherits ALL source participants, copies messages,
138
+ * creator becomes owner, and forks the source AI session when AI is enabled.
139
+ * - spawn (copyHistory=false): config only, creator is sole owner, empty history.
140
+ * It still creates a fresh AI session when AI is enabled.
141
+ * - subagent (copyHistory=false + opts.subagent): agent-initiated worker room.
142
+ * Marked `isSubagent`, optionally overriding the child's agent/model.
143
+ *
144
+ * @param sourceTopicId - must exist and caller must be a participant
145
+ * @param userId - caller (becomes owner of the new topic)
146
+ * @param copyHistory - true for fork, false for spawn
147
+ * @param opts - optional custom name and subagent overrides
148
+ * @returns the newly created TopicDto or null on error
149
+ * @throws TopicDeriveBusyError when user-triggered derivation races active work or maintenance
150
+ * @throws TopicTitleConflictError when the requested name is already taken
151
+ */
152
+ export async function createDerivedTopic(
153
+ sourceTopicId: string,
154
+ userId: string,
155
+ copyHistory: boolean,
156
+ opts?: DerivedTopicOptions,
157
+ ): Promise<TopicDto | null> {
158
+ const topic = getTopic(sourceTopicId);
159
+ if (!topic) return null;
160
+ if (topic.kind === "manager") return null;
161
+ if (!isParticipant(topic, userId)) return null;
162
+
163
+ const subagent = copyHistory ? undefined : opts?.subagent;
164
+ let maintenance: ReturnType<typeof beginRuntimeTopicMaintenance> = null;
165
+ if (!subagent && !opts?.allowActiveSource) {
166
+ if (isTopicRunning(sourceTopicId)) throw new TopicDeriveBusyError();
167
+ maintenance = beginRuntimeTopicMaintenance(sourceTopicId);
168
+ if (!maintenance) throw new TopicDeriveBusyError();
169
+ if (isTopicRunning(sourceTopicId)) {
170
+ maintenance.finish();
171
+ maintenance = null;
172
+ throw new TopicDeriveBusyError();
173
+ }
174
+ }
175
+
176
+ try {
177
+ return await createDerivedTopicUnderFence(topic, sourceTopicId, userId, copyHistory, opts);
178
+ } finally {
179
+ maintenance?.finish();
180
+ }
181
+ }
182
+
183
+ async function createDerivedTopicUnderFence(
184
+ topic: TopicDto,
185
+ sourceTopicId: string,
186
+ userId: string,
187
+ copyHistory: boolean,
188
+ opts?: DerivedTopicOptions,
189
+ ): Promise<TopicDto | null> {
190
+ const now = new Date().toISOString();
191
+ const subagent = copyHistory ? undefined : opts?.subagent;
192
+ const suffix = copyHistory ? "fork" : subagent ? "agent" : "spawn";
193
+ const sourceConfig = getApiTopicConfig(sourceTopicId);
194
+ const agent = subagent?.agent ?? effectiveAgentForTopic(sourceTopicId, topic);
195
+
196
+ // Fork: inherit all source participants, creator becomes owner
197
+ // Spawn: creator is sole owner
198
+ // Subagent: inherit all source participants — everyone who can see the
199
+ // parent room sees the card there, so its "view room" target must be
200
+ // accessible to them too (single-topic GET rejects non-participants).
201
+ const participants: TopicDto["participants"] =
202
+ copyHistory || subagent
203
+ ? [
204
+ ...topic.participants
205
+ .filter((p) => p.userId !== userId)
206
+ .map((p) => ({ ...p, role: "member" as const })),
207
+ { userId, role: "owner" as const },
208
+ ]
209
+ : [{ userId, role: "owner" as const }];
210
+ const kind = topic.kind ?? inferTopicKind(topic);
211
+ const title = opts?.name?.trim() || nextDerivedTopicTitle(topic.title, kind, suffix);
212
+ const conflict = findTopicTitleConflict(title, kind);
213
+ if (conflict) {
214
+ logger.info(
215
+ { sourceTopicId, title, kind, conflictTopicId: conflict.id },
216
+ "createDerivedTopic: title conflict",
217
+ );
218
+ throw new TopicTitleConflictError(title);
219
+ }
220
+ const derived: TopicDto = {
221
+ id: randomUUID(),
222
+ title,
223
+ kind,
224
+ description: topic.description,
225
+ agent: subagent?.agent ?? topic.agent,
226
+ defaultModel: subagent?.model ?? topic.defaultModel,
227
+ defaultEffort: topic.defaultEffort,
228
+ aiMode: topic.aiMode,
229
+ aiMention: topic.aiMention,
230
+ participants,
231
+ createdAt: now,
232
+ lastMessageAt: now,
233
+ parentTopicId: sourceTopicId,
234
+ isFork: copyHistory,
235
+ ...(subagent ? { isSubagent: true } : {}),
236
+ visibility: topic.visibility,
237
+ accessMode: topic.accessMode,
238
+ };
239
+
240
+ let sessionId: string | undefined;
241
+ let rollbackHandle: ForkHandle | undefined;
242
+
243
+ try {
244
+ if (agent) {
245
+ const cwd = resolveTopicWorkspaceDir(derived.id);
246
+ mkdirSync(cwd, { recursive: true });
247
+ const registry = getRegistry(agent);
248
+ const requestedRolloutModel =
249
+ subagent?.model ??
250
+ (subagent?.agent ? undefined : sourceConfig?.model) ??
251
+ derived.defaultModel;
252
+ const rolloutModel = resolveModelForAgent(agent, requestedRolloutModel, registry);
253
+ const requestedRolloutEffort = subagent?.agent
254
+ ? registry.defaultEffort
255
+ : (sourceConfig?.effort ?? derived.defaultEffort);
256
+ const rolloutEffort =
257
+ requestedRolloutEffort && registry.validateEffort(requestedRolloutEffort)
258
+ ? requestedRolloutEffort
259
+ : registry.defaultEffort;
260
+
261
+ if (copyHistory) {
262
+ const parentSessionId = getTopicSessionId(sourceTopicId);
263
+ if (parentSessionId) {
264
+ try {
265
+ const fork = await forkAgentSession({
266
+ agent,
267
+ parentSessionId,
268
+ cwd,
269
+ userId,
270
+ topicName: topic.title,
271
+ title,
272
+ model: rolloutModel,
273
+ ...(rolloutEffort ? { effort: rolloutEffort } : {}),
274
+ });
275
+ sessionId = fork.forkId;
276
+ rollbackHandle = fork;
277
+ } catch (err) {
278
+ logger.warn(
279
+ { err, sourceTopicId, title: topic.title, agent, parentSessionId },
280
+ "createDerivedTopic: native fork failed; synthesizing from unified history",
281
+ );
282
+ }
283
+ }
284
+ if (!sessionId) {
285
+ const rollout = registry.writeRollout({
286
+ cwd,
287
+ entries: readConversation(userId, topic.title),
288
+ model: rolloutModel,
289
+ ...(rolloutEffort ? { effort: rolloutEffort } : {}),
290
+ });
291
+ sessionId = rollout.sessionId;
292
+ rollbackHandle = rollbackHandleFor(agent, rollout.sessionId, rollout.rolloutPath);
293
+ }
294
+ } else {
295
+ const rollout = registry.writeRollout({
296
+ cwd,
297
+ entries: [],
298
+ model: rolloutModel,
299
+ ...(rolloutEffort ? { effort: rolloutEffort } : {}),
300
+ });
301
+ sessionId = rollout.sessionId;
302
+ rollbackHandle = rollbackHandleFor(agent, rollout.sessionId, rollout.rolloutPath);
303
+ }
304
+ }
305
+
306
+ const created = db
307
+ .transaction(() => {
308
+ const currentSource = getTopic(sourceTopicId);
309
+ if (
310
+ !currentSource ||
311
+ currentSource.kind === "manager" ||
312
+ !isParticipant(currentSource, userId)
313
+ ) {
314
+ throw new TopicDeriveBusyError("Source topic changed while deriving; try again");
315
+ }
316
+ const transactionalConflict = findTopicTitleConflict(title, kind);
317
+ if (transactionalConflict) throw new TopicTitleConflictError(title);
318
+ upsertTopic(derived);
319
+ if (subagent) {
320
+ // Subagent worker rooms do NOT cascade the parent's optional MCP
321
+ // whitelist — they start with the default (required-only) MCP set,
322
+ // mirroring clawgram's subagent policy. An explicit agent/model
323
+ // override must also beat the parent's config override, which
324
+ // startAiTurn resolves ahead of the topic defaults; dropping the
325
+ // inherited model when only the agent changes avoids pinning the
326
+ // child to a model that belongs to the parent's agent.
327
+ const childConfig = { ...(sourceConfig ?? {}) };
328
+ delete childConfig.mcp;
329
+ if (subagent.agent) {
330
+ delete childConfig.model;
331
+ }
332
+ if (subagent.model) childConfig.model = subagent.model;
333
+ if (Object.keys(childConfig).length > 0) setApiTopicConfig(derived.id, childConfig);
334
+ } else if (sourceConfig) {
335
+ setApiTopicConfig(derived.id, sourceConfig);
336
+ }
337
+ if (copyHistory) {
338
+ copyMessagesForTopic(sourceTopicId, derived.id);
339
+ cloneConversationLog({
340
+ userId,
341
+ srcTopic: topic.title,
342
+ dstTopic: derived.title,
343
+ });
344
+ }
345
+ if (sessionId && agent) {
346
+ setTopicSessionId(derived.id, sessionId, {
347
+ reason: copyHistory ? "slash-fork" : subagent ? "spawn-subagent" : "slash-spawn",
348
+ agent,
349
+ });
350
+ appendConversationEventStrict(userId, derived.title, agent, {
351
+ type: "session",
352
+ sessionId,
353
+ });
354
+ }
355
+ return derived;
356
+ })
357
+ .immediate();
358
+
359
+ // Derived rooms inherit the browser profile only when the creator already
360
+ // owns the source. Crossing an owner boundary always starts with a fresh
361
+ // profile so cookies and login state cannot be transferred by a member.
362
+ try {
363
+ const profileClone = await cloneProfileForChild({
364
+ userId,
365
+ srcTopic: sourceTopicId,
366
+ dstTopic: derived.id,
367
+ });
368
+ logger.info(
369
+ {
370
+ sourceTopicId,
371
+ derivedTopicId: derived.id,
372
+ copyHistory,
373
+ subagent: Boolean(subagent),
374
+ profileClone,
375
+ },
376
+ "createDerivedTopic: playwright profile clone",
377
+ );
378
+ } catch (profileErr) {
379
+ logger.warn(
380
+ { err: profileErr, sourceTopicId, derivedTopicId: derived.id },
381
+ "createDerivedTopic: playwright profile clone threw (continuing)",
382
+ );
383
+ }
384
+
385
+ // Every derived room (fork/spawn/subagent) is user-visible: tell all
386
+ // inherited participants, not just the client that issued the request.
387
+ WsHub.get().broadcastTopicCreated(created);
388
+ return created;
389
+ } catch (err) {
390
+ if (rollbackHandle) cleanupAgentFork(rollbackHandle);
391
+ if (err instanceof TopicDeriveBusyError || err instanceof TopicTitleConflictError) throw err;
392
+ logger.warn(
393
+ { err, sourceTopicId, derivedTopicId: derived.id, copyHistory },
394
+ "createDerivedTopic: failed to create derived topic",
395
+ );
396
+ return null;
397
+ }
398
+ }