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,352 @@
1
+ /**
2
+ * Format a tool_use event into a human-readable label.
3
+ *
4
+ * Agent-agnostic: the SDKs share the same convention of `(name, input record)`
5
+ * for tool calls, so this lives next to the providers rather than inside any
6
+ * one of them. Used by progress messages, fork relays, and the conversation
7
+ * event processor.
8
+ */
9
+ export type ToolCallSummaryValue =
10
+ | string
11
+ | number
12
+ | boolean
13
+ | Array<{ label: string; description?: string }>;
14
+
15
+ export type ToolCallSummaryInput = Record<string, ToolCallSummaryValue>;
16
+
17
+ const TOOL_SUMMARY_MAX_CHARS = 90;
18
+ const TOOL_SUMMARY_HEAD_CHARS = 52;
19
+ const TOOL_SUMMARY_TAIL_CHARS = 28;
20
+ const SHELL_SUMMARY_MAX_PARTS = 3;
21
+
22
+ const SUMMARY_KEYS = [
23
+ "command",
24
+ "cmd",
25
+ "file_path",
26
+ "file_id",
27
+ "path",
28
+ "image_path",
29
+ "url",
30
+ "query",
31
+ "pattern",
32
+ "question",
33
+ "task",
34
+ "description",
35
+ "title",
36
+ "theme",
37
+ "alt",
38
+ "to",
39
+ ] as const;
40
+
41
+ export function summarizeDisplayText(value: string): string {
42
+ const normalized = value.replace(/\s+/g, " ").trim();
43
+ if (!normalized) return "";
44
+ if (normalized.length <= TOOL_SUMMARY_MAX_CHARS) return normalized;
45
+ return `${normalized.slice(0, TOOL_SUMMARY_HEAD_CHARS).trimEnd()}...${normalized
46
+ .slice(-TOOL_SUMMARY_TAIL_CHARS)
47
+ .trimStart()}`;
48
+ }
49
+
50
+ function unwrapShellCommand(value: string): string {
51
+ const normalized = value.replace(/\s+/g, " ").trim();
52
+ const match = normalized.match(/^(?:\/[^\s]+\/)?(?:zsh|bash|sh)\s+-[a-z]*c\s+(.+)$/i);
53
+ if (!match?.[1]) return normalized;
54
+ const payload = match[1].trim();
55
+ const quote = payload[0];
56
+ return (quote === '"' || quote === "'") && payload.at(-1) === quote
57
+ ? payload.slice(1, -1).trim()
58
+ : payload;
59
+ }
60
+
61
+ function splitShellCommands(value: string): string[] {
62
+ const commands: string[] = [];
63
+ let current = "";
64
+ let quote = "";
65
+ let escaped = false;
66
+ const flush = () => {
67
+ const command = current.trim();
68
+ if (command) commands.push(command);
69
+ current = "";
70
+ };
71
+
72
+ for (let i = 0; i < value.length; i++) {
73
+ const char = value[i] ?? "";
74
+ if (escaped) {
75
+ current += char;
76
+ escaped = false;
77
+ continue;
78
+ }
79
+ if (char === "\\" && quote !== "'") {
80
+ current += char;
81
+ escaped = true;
82
+ continue;
83
+ }
84
+ if (quote) {
85
+ current += char;
86
+ if (char === quote) quote = "";
87
+ continue;
88
+ }
89
+ if (char === '"' || char === "'") {
90
+ quote = char;
91
+ current += char;
92
+ continue;
93
+ }
94
+ const pair = value.slice(i, i + 2);
95
+ if (pair === "&&" || pair === "||") {
96
+ flush();
97
+ i += 1;
98
+ continue;
99
+ }
100
+ if (char === ";") {
101
+ flush();
102
+ continue;
103
+ }
104
+ current += char;
105
+ }
106
+ flush();
107
+ return commands;
108
+ }
109
+
110
+ function shellWords(value: string): string[] {
111
+ const words: string[] = [];
112
+ let current = "";
113
+ let quote = "";
114
+ let escaped = false;
115
+ const flush = () => {
116
+ if (current) words.push(current);
117
+ current = "";
118
+ };
119
+
120
+ for (const char of value) {
121
+ if (escaped) {
122
+ current += char;
123
+ escaped = false;
124
+ continue;
125
+ }
126
+ if (char === "\\" && quote !== "'") {
127
+ escaped = true;
128
+ continue;
129
+ }
130
+ if (quote) {
131
+ if (char === quote) quote = "";
132
+ else current += char;
133
+ continue;
134
+ }
135
+ if (char === '"' || char === "'") {
136
+ quote = char;
137
+ continue;
138
+ }
139
+ if (/\s/.test(char)) {
140
+ flush();
141
+ continue;
142
+ }
143
+ current += char;
144
+ }
145
+ flush();
146
+ return words;
147
+ }
148
+
149
+ function commandName(value: string): string {
150
+ return value.replace(/\\/g, "/").split("/").at(-1) || value;
151
+ }
152
+
153
+ function compactPath(value: string): string {
154
+ const normalized = value.replace(/\\/g, "/").replace(/\/$/, "");
155
+ return normalized.split("/").at(-1) || normalized;
156
+ }
157
+
158
+ function looksLikePath(value: string): boolean {
159
+ return (
160
+ value.includes("/") ||
161
+ /^\.?[A-Za-z0-9_-]+\.(?:[A-Za-z0-9_-]+)$/.test(value) ||
162
+ /^(?:CLAUDE|AGENTS|README|STYLE)(?:\.[A-Za-z0-9_-]+)?$/i.test(value)
163
+ );
164
+ }
165
+
166
+ function summarizeTargets(values: string[]): string {
167
+ const targets = values.filter((value) => value && !value.startsWith("-")).map(compactPath);
168
+ if (targets.length === 0) return "";
169
+ if (targets.length === 1) return targets[0] ?? "";
170
+ return `${targets[0]} +${targets.length - 1}`;
171
+ }
172
+
173
+ function summarizeShellPart(value: string): string {
174
+ const words = shellWords(value).filter((word) => !/^\d*>/.test(word));
175
+ while (words[0] === "sudo") words.shift();
176
+ if (words[0] === "env") {
177
+ words.shift();
178
+ while (words[0] && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || words[0].startsWith("-"))) {
179
+ words.shift();
180
+ }
181
+ }
182
+ const executable = commandName(words.shift() ?? "");
183
+ if (!executable || executable === "true" || executable === "printf" || executable === "echo") {
184
+ return "";
185
+ }
186
+
187
+ if (executable === "pwd") return "pwd";
188
+ if (executable === "command" && words[0] === "-v") {
189
+ return words[1] ? `find ${commandName(words[1])}` : "find command";
190
+ }
191
+ if (executable === "git") {
192
+ while (words[0]?.startsWith("-")) {
193
+ const option = words.shift();
194
+ if (
195
+ option === "-C" ||
196
+ option === "-c" ||
197
+ option === "--git-dir" ||
198
+ option === "--work-tree"
199
+ ) {
200
+ words.shift();
201
+ }
202
+ }
203
+ const operation = words.shift();
204
+ const targets = words.filter((word) => looksLikePath(word));
205
+ const target = summarizeTargets(targets);
206
+ return ["git", operation, target].filter(Boolean).join(" ");
207
+ }
208
+ if (executable === "sed") {
209
+ const target = summarizeTargets(words.filter(looksLikePath));
210
+ return target ? `sed ${target}` : "sed";
211
+ }
212
+ if (executable === "rg") {
213
+ const filesOnly = words.includes("--files");
214
+ const values = words.filter((word) => !word.startsWith("-"));
215
+ const target = summarizeTargets(values);
216
+ return filesOnly ? ["rg files", target].filter(Boolean).join(" ") : `rg ${target || "search"}`;
217
+ }
218
+ if (
219
+ executable === "bun" ||
220
+ executable === "npm" ||
221
+ executable === "pnpm" ||
222
+ executable === "yarn"
223
+ ) {
224
+ const operation = words[0];
225
+ if (operation === "test") {
226
+ const targets = words.slice(1).filter((word) => !word.startsWith("-"));
227
+ return targets.length > 0
228
+ ? `${executable} test ${targets.length} files`
229
+ : `${executable} test`;
230
+ }
231
+ return [executable, operation].filter(Boolean).join(" ");
232
+ }
233
+ if (executable === "ls" || executable === "cat" || executable === "node") {
234
+ const target = summarizeTargets(words.filter(looksLikePath));
235
+ return [executable, target].filter(Boolean).join(" ");
236
+ }
237
+
238
+ const detail = words.find((word) => !word.startsWith("-"));
239
+ return [executable, detail ? compactPath(detail) : ""].filter(Boolean).join(" ");
240
+ }
241
+
242
+ /** Convert a raw Codex/Claude shell invocation into a bounded display label. */
243
+ export function summarizeShellCommand(value: string): string {
244
+ const payload = unwrapShellCommand(value);
245
+ const parts = splitShellCommands(payload).map(summarizeShellPart).filter(Boolean);
246
+ if (parts.length === 0) return summarizeDisplayText(payload);
247
+ const visible = parts.slice(0, SHELL_SUMMARY_MAX_PARTS);
248
+ const remainder = parts.length - visible.length;
249
+ return summarizeDisplayText(`${visible.join(" · ")}${remainder > 0 ? ` · +${remainder}` : ""}`);
250
+ }
251
+
252
+ function summarizePrimitive(value: unknown): string | number | boolean | undefined {
253
+ if (typeof value === "number" || typeof value === "boolean") return value;
254
+ if (typeof value !== "string") return undefined;
255
+ return summarizeDisplayText(value) || undefined;
256
+ }
257
+
258
+ function summarizeAskChoices(
259
+ value: unknown,
260
+ ): Array<{ label: string; description?: string }> | undefined {
261
+ if (!Array.isArray(value)) return undefined;
262
+ const choices = value
263
+ .map((choice) => {
264
+ if (!choice || typeof choice !== "object") return null;
265
+ const record = choice as Record<string, unknown>;
266
+ const label = summarizePrimitive(record.label);
267
+ if (typeof label !== "string") return null;
268
+ const description = summarizePrimitive(record.description);
269
+ return {
270
+ label,
271
+ ...(typeof description === "string" ? { description } : {}),
272
+ };
273
+ })
274
+ .filter((choice): choice is { label: string; description?: string } => choice !== null)
275
+ .slice(0, 6);
276
+ return choices.length > 0 ? choices : undefined;
277
+ }
278
+
279
+ export function summarizeToolInput(
280
+ name: string,
281
+ input: Record<string, unknown>,
282
+ ): ToolCallSummaryInput | undefined {
283
+ const summary: ToolCallSummaryInput = {};
284
+ const shortName = name.split("__").at(-1)?.toLowerCase() ?? name.toLowerCase();
285
+
286
+ if (name === "AskUserQuestion") {
287
+ const question = summarizePrimitive(input.question);
288
+ if (typeof question === "string") summary.question = question;
289
+ const choices = summarizeAskChoices(input.choices);
290
+ if (choices) summary.choices = choices;
291
+ return Object.keys(summary).length > 0 ? summary : undefined;
292
+ }
293
+
294
+ for (const key of SUMMARY_KEYS) {
295
+ const raw = input[key];
296
+ const value =
297
+ (key === "command" || key === "cmd") && typeof raw === "string"
298
+ ? summarizeShellCommand(raw)
299
+ : summarizePrimitive(raw);
300
+ if (value !== undefined) summary[key] = value;
301
+ }
302
+
303
+ if (shortName === "ask_session" || shortName === "tell_session") {
304
+ const message = summarizePrimitive(input.message);
305
+ if (typeof message === "string") summary.message = message;
306
+ }
307
+
308
+ if (shortName === "edit") {
309
+ const before = summarizePrimitive(input.old_string);
310
+ const after = summarizePrimitive(input.new_string);
311
+ if (typeof before === "string") summary.before = before;
312
+ if (typeof after === "string") summary.after = after;
313
+ }
314
+ if (shortName === "write") {
315
+ const content = typeof input.content === "string" ? input.content : undefined;
316
+ const preview = summarizePrimitive(content);
317
+ if (typeof preview === "string") summary.preview = preview;
318
+ if (content !== undefined) summary.lines = content.split("\n").length;
319
+ }
320
+
321
+ return Object.keys(summary).length > 0 ? summary : undefined;
322
+ }
323
+
324
+ export function formatToolUse(name: string, input: Record<string, unknown>): string {
325
+ let detail = "";
326
+ if (name === "View" && input.question) {
327
+ const file = input.image_path || input.file_path || input.path;
328
+ const fileName = typeof file === "string" ? file.split("/").pop() : "";
329
+ const suffix = fileName ? ` [${fileName}]` : "";
330
+ detail = `${String(input.question)}${suffix}`;
331
+ } else if ((name === "Agent" || name === "Task") && input.subagent_type) {
332
+ const type = String(input.subagent_type);
333
+ const desc = input.description ? ` ${String(input.description).slice(0, 60)}` : "";
334
+ detail = `[${type}]${desc}`;
335
+ } else if (input.command || input.cmd) {
336
+ detail = summarizeShellCommand(String(input.command || input.cmd));
337
+ } else if (input.file_path || input.path) detail = String(input.file_path || input.path);
338
+ else if (input.file_id) detail = String(input.file_id);
339
+ else if (input.url) detail = String(input.url);
340
+ else if (input.pattern) detail = String(input.pattern);
341
+ else if (input.query || input.text) detail = String(input.query || input.text);
342
+ else if (input.task) detail = String(input.task);
343
+ else if (input.title) detail = String(input.title);
344
+ else if (input.to) detail = String(input.to);
345
+ else if (input.message) detail = String(input.message).slice(0, 80);
346
+ else if (input.content) detail = String(input.content).slice(0, 80);
347
+
348
+ if (detail) {
349
+ return `${name}(${detail.length > 100 ? `${detail.slice(0, 100)}...` : detail})`;
350
+ }
351
+ return name;
352
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Atomic agent-switch operation for a forum topic.
3
+ *
4
+ * Shared by API routes and runtime self-config tools when switching a topic's
5
+ * agent backend. Both call sites share:
6
+ *
7
+ * - rollout bridging (writes a synthetic native rollout for the target
8
+ * SDK so the new backend resumes with the cross-agent conversation),
9
+ * - atomic UPDATE of `agent` + `session_id` so a concurrent query can
10
+ * never observe the new agent paired with the stale old-agent session,
11
+ * - orphan-rollout cleanup if the DB commit fails after the rollout file
12
+ * was already written.
13
+ *
14
+ * Every code path returns a structured result so callers can format their
15
+ * own response. Logging stays inside this module so callers do not have to
16
+ * duplicate the diagnostic context.
17
+ */
18
+ import { unlinkSync } from "node:fs";
19
+ import { checkAgentAuth } from "#agents/auth-check";
20
+ import { isAgentKind } from "#agents/index";
21
+ import { resolveModelForAgent } from "#agents/model-catalog";
22
+ import { getRegistry } from "#agents/registry";
23
+ import { SESSION_WORKSPACE_DIR } from "#platform/config";
24
+ import { logger } from "#platform/logger";
25
+ import {
26
+ appendConversationEventStrict,
27
+ findLastSessionIdForAgent,
28
+ readConversation,
29
+ } from "#storage/conversations";
30
+ import {
31
+ getTopicAgentSwitchState,
32
+ setTopicAgentAndClearSession,
33
+ setTopicAgentAndSession,
34
+ } from "#storage/topic-settings";
35
+ import type { AgentKind } from "#types";
36
+
37
+ export type SwitchAgentOutcome =
38
+ | { kind: "noop"; agent: AgentKind }
39
+ | {
40
+ kind: "fresh";
41
+ agent: AgentKind;
42
+ reason: "no-history" | "bridge-failed";
43
+ }
44
+ | {
45
+ kind: "bridged";
46
+ agent: AgentKind;
47
+ bridgedSessionId: string;
48
+ };
49
+
50
+ export type SwitchAgentResult =
51
+ | { ok: true; outcome: SwitchAgentOutcome }
52
+ | { ok: false; error: string };
53
+
54
+ /**
55
+ * Switch the active agent for a topic, bridging conversation history into a
56
+ * native rollout for the target SDK when prior turns exist. Idempotent: a
57
+ * call that targets the current agent returns `{kind: "noop"}` without
58
+ * touching the DB or filesystem.
59
+ */
60
+ export function switchTopicAgent(
61
+ userId: number,
62
+ topicName: string,
63
+ agent: AgentKind,
64
+ ): SwitchAgentResult {
65
+ if (!isAgentKind(agent)) {
66
+ return { ok: false, error: `invalid agent '${agent}'` };
67
+ }
68
+ const topic = getTopicAgentSwitchState(userId, topicName);
69
+ if (!topic) {
70
+ return { ok: false, error: `topic "${topicName}" not found` };
71
+ }
72
+ if (topic.agent === agent) {
73
+ return { ok: true, outcome: { kind: "noop", agent } };
74
+ }
75
+
76
+ // Auth precondition — block the switch if the target backend isn't
77
+ // authenticated, otherwise the topic flips and every subsequent turn
78
+ // fails inside the provider with an opaque per-turn error.
79
+ const auth = checkAgentAuth(agent);
80
+ if (!auth.ok) return { ok: false, error: auth.error };
81
+
82
+ const oldAgent = topic.agent;
83
+
84
+ const conversationEntries = readConversation(userId, topicName);
85
+
86
+ // No prior history → simple switch, clear session, fresh start.
87
+ if (conversationEntries.length === 0) {
88
+ const changed = setTopicAgentAndClearSession({
89
+ userId,
90
+ topicName,
91
+ agent,
92
+ });
93
+ if (!changed) return { ok: false, error: `failed to set agent for "${topicName}"` };
94
+ return {
95
+ ok: true,
96
+ outcome: { kind: "fresh", agent, reason: "no-history" },
97
+ };
98
+ }
99
+
100
+ // History exists → synthesize a native rollout for the target SDK so the
101
+ // new backend resumes with the cross-agent conversation already loaded.
102
+ //
103
+ // If the user has been on the target agent before (e.g. claude → codex →
104
+ // claude round-trip), reuse the most recent session id for that agent so
105
+ // the synthetic file lands at the path the SDK already manages. This
106
+ // avoids two failure modes:
107
+ // 1. Orphan SDK rollouts piling up under `~/.claude/projects/<dir>/`
108
+ // (one per round-trip) without ever being resumed again.
109
+ // 2. The SDK's prompt-cache resetting each switch because the resume
110
+ // key changes — even though logical history is identical.
111
+ const cwd = SESSION_WORKSPACE_DIR;
112
+ const reuseSessionId = findLastSessionIdForAgent(conversationEntries, agent) ?? undefined;
113
+ let bridgedSessionId: string;
114
+ let rolloutPath: string;
115
+ try {
116
+ const registry = getRegistry(agent);
117
+ const result = registry.writeRollout({
118
+ cwd,
119
+ entries: conversationEntries,
120
+ model: resolveModelForAgent(agent, undefined, registry),
121
+ ...(registry.defaultEffort ? { effort: registry.defaultEffort } : {}),
122
+ ...(reuseSessionId ? { reuseSessionId } : {}),
123
+ });
124
+ bridgedSessionId = result.sessionId;
125
+ rolloutPath = result.rolloutPath;
126
+ } catch (err) {
127
+ logger.warn(
128
+ { err, userId, topicName, from: oldAgent, to: agent },
129
+ "switchTopicAgent: rollout encoding failed — falling back to fresh session",
130
+ );
131
+ const changed = setTopicAgentAndClearSession({
132
+ userId,
133
+ topicName,
134
+ agent,
135
+ });
136
+ if (!changed) return { ok: false, error: `failed to set agent for "${topicName}"` };
137
+ return {
138
+ ok: true,
139
+ outcome: { kind: "fresh", agent, reason: "bridge-failed" },
140
+ };
141
+ }
142
+
143
+ // Manifest the synthetic rollout before the DB commit. This is not a true
144
+ // SQLite+filesystem transaction, but it avoids the dangerous split-brain
145
+ // state where DB.session_id points at a rollout that the unified manifest
146
+ // cannot discover for round-trip reuse or cleanup.
147
+ try {
148
+ appendConversationEventStrict(userId, topicName, agent, {
149
+ type: "session",
150
+ sessionId: bridgedSessionId,
151
+ });
152
+ } catch (err) {
153
+ try {
154
+ unlinkSync(rolloutPath);
155
+ } catch (_) {
156
+ // best-effort
157
+ }
158
+ logger.warn(
159
+ { err, userId, topicName, rolloutPath },
160
+ "switchTopicAgent: manifest append failed, removed orphan rollout",
161
+ );
162
+ return { ok: false, error: `failed to record agent switch for "${topicName}"` };
163
+ }
164
+
165
+ // Atomic DB commit: agent and session_id in one transaction. If it fails,
166
+ // unlink the orphan rollout. The manifest entry
167
+ // above may remain, but cleanup tolerates ENOENT and future bridge attempts
168
+ // can safely reuse that id by overwriting the rollout.
169
+ try {
170
+ setTopicAgentAndSession({
171
+ userId,
172
+ topicName,
173
+ agent,
174
+ sessionId: bridgedSessionId,
175
+ });
176
+ } catch (err) {
177
+ try {
178
+ unlinkSync(rolloutPath);
179
+ } catch (_) {
180
+ // best-effort
181
+ }
182
+ logger.warn(
183
+ { err, userId, topicName, rolloutPath },
184
+ "switchTopicAgent: DB commit failed, removed orphan rollout",
185
+ );
186
+ return { ok: false, error: `failed to commit agent switch for "${topicName}"` };
187
+ }
188
+
189
+ return {
190
+ ok: true,
191
+ outcome: { kind: "bridged", agent, bridgedSessionId },
192
+ };
193
+ }