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,18 @@
1
+ /** "from" field for session-inbox entries injected after a setting change. */
2
+ export const FROM_AUTO_CONTINUE = "auto-continue";
3
+
4
+ /** "from" field for durable delayed continuations created by schedule_self. */
5
+ export const FROM_SELF_SCHEDULE = "self-schedule";
6
+
7
+ /** Topic names that collide with internal sentinels and must not be used. */
8
+ export const RESERVED_TOPIC_NAMES: ReadonlySet<string> = new Set([
9
+ FROM_AUTO_CONTINUE,
10
+ FROM_SELF_SCHEDULE,
11
+ "general",
12
+ ]);
13
+
14
+ /** Legacy shared General id. New General rooms use per-user UUIDs. */
15
+ export const GENERAL_TOPIC_ID = "general";
16
+
17
+ /** Internal topic name for the forum manager session (General topic). */
18
+ export const TOPIC_MANAGER = "__manager__";
@@ -0,0 +1,14 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { DEBUG_FILE } from "#platform/config";
3
+ import { readJsonFile } from "#platform/jsonl";
4
+
5
+ /** Load the set of debug-enabled user IDs from disk. Returns empty set on error. */
6
+ export function loadDebugUsers(): Set<string> {
7
+ const arr = readJsonFile<string[]>(DEBUG_FILE);
8
+ return new Set(arr ?? []);
9
+ }
10
+
11
+ /** Persist the debug users set to disk. */
12
+ export function saveDebugUsers(users: Set<string>): void {
13
+ writeFileSync(DEBUG_FILE, JSON.stringify([...users], null, 2));
14
+ }
@@ -0,0 +1 @@
1
+ export const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Stringify an unknown thrown value for logging or user-visible messages.
3
+ * Default fallback is `String(e)`. Pass an explicit fallback (e.g. "unknown")
4
+ * to override what non-Error throws turn into.
5
+ */
6
+ export function errMsg(e: unknown, fallback?: string): string {
7
+ if (e instanceof Error) return e.message;
8
+ return fallback ?? String(e);
9
+ }
@@ -0,0 +1,16 @@
1
+ import { unlinkSync } from "node:fs";
2
+ import { logger } from "#platform/logger";
3
+
4
+ /**
5
+ * Best-effort unlink. ENOENT is always swallowed (the file is already gone).
6
+ * Other errors are silent unless `warnLabel` is provided, in which case they
7
+ * are logged at warn level with `{ err, path }` context.
8
+ */
9
+ export function safeUnlink(path: string, warnLabel?: string): void {
10
+ try {
11
+ unlinkSync(path);
12
+ } catch (e) {
13
+ if ((e as NodeJS.ErrnoException)?.code === "ENOENT") return;
14
+ if (warnLabel) logger.warn({ err: e, path }, warnLabel);
15
+ }
16
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ appendFileSync,
3
+ closeSync,
4
+ fsyncSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { dirname } from "node:path";
14
+ import { safeUnlink } from "#platform/file-utils";
15
+
16
+ export function readJsonlLines(filePath: string): string[] {
17
+ return readFileSync(filePath, "utf-8").trim().split("\n").filter(Boolean);
18
+ }
19
+
20
+ /**
21
+ * Parse a JSONL string (one JSON value per line) into an array of `T`.
22
+ * Throws on malformed lines — use when a corrupt fixture/log should fail
23
+ * loudly rather than silently drop entries (paired with `readJsonlLines`'
24
+ * resilient counterpart for live data streams).
25
+ */
26
+ export function parseJsonlText<T = unknown>(raw: string): T[] {
27
+ return raw
28
+ .trim()
29
+ .split("\n")
30
+ .filter(Boolean)
31
+ .map((line) => JSON.parse(line) as T);
32
+ }
33
+
34
+ /**
35
+ * Read and JSON-parse a file. Returns null if the file is missing or contains
36
+ * invalid JSON. Use when a missing/corrupt file should fall through to a
37
+ * default rather than throw.
38
+ */
39
+ export function readJsonFile<T = unknown>(filePath: string): T | null {
40
+ try {
41
+ return JSON.parse(readFileSync(filePath, "utf-8")) as T;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // --- Cross-process append lock ---
48
+ // POSIX `O_APPEND` is only atomic when the write payload fits in PIPE_BUF
49
+ // (Linux 4096, macOS 512). MCP servers and the bot run as separate
50
+ // processes that all `appendFileSync` to shared inbox/outbox files; a long
51
+ // `tell_session` prompt or large `ask_session` reply is comfortably above
52
+ // PIPE_BUF, and a concurrent append from another process can interleave at
53
+ // the byte level — the result fails JSON parse and the entire entry is
54
+ // silently dropped by `parseOutboxLine`.
55
+ //
56
+ // Mitigation: a sidecar `<filePath>.lock` created with `O_EXCL` serializes
57
+ // every append at the OS level. Lock is held for the duration of one
58
+ // `appendFileSync`, so the contention window is microseconds. Stale locks
59
+ // (process crashed mid-write) are detected via mtime and forcibly removed
60
+ // after `LOCK_STALE_MS`. As a last resort, after `LOCK_TIMEOUT_MS` of
61
+ // retries, the lock is bypassed — the entry survives but may interleave;
62
+ // preferable to dropping it entirely.
63
+ const LOCK_SUFFIX = ".lock";
64
+ const LOCK_RETRY_MS = 5;
65
+ const LOCK_TIMEOUT_MS = 1500;
66
+ const LOCK_STALE_MS = 5000;
67
+ const LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
68
+
69
+ /** Synchronous, non-spinning sleep supported by both Bun and Node MCP workers. */
70
+ function sleepForAppendLock(ms: number): void {
71
+ Atomics.wait(LOCK_SLEEP, 0, 0, ms);
72
+ }
73
+
74
+ function tryAcquireAppendLock(lockPath: string): boolean {
75
+ try {
76
+ closeSync(openSync(lockPath, "wx")); // O_CREAT | O_EXCL — atomic
77
+ return true;
78
+ } catch (e) {
79
+ if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function isStaleLock(lockPath: string): boolean {
85
+ try {
86
+ return Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Append a JSON-encoded entry + newline to a JSONL file. Ensures the parent
94
+ * directory exists and serializes concurrent multi-process appends through a
95
+ * sidecar lock so payloads larger than PIPE_BUF do not interleave.
96
+ *
97
+ * The drainer side (`outbox/file-ops.ts`) claims the file via atomic rename
98
+ * and handles recovery of leftover `.processing` files from crashes.
99
+ */
100
+ export function appendJsonlEntry(filePath: string, entry: unknown): void {
101
+ appendJsonlLine(filePath, `${JSON.stringify(entry)}\n`);
102
+ }
103
+
104
+ /**
105
+ * Append an already-serialized JSONL line (must end with "\n") under the
106
+ * same cross-process append lock as `appendJsonlEntry`. Use when you
107
+ * want to write back a raw line **exactly** — e.g. flusher tail-writeback
108
+ * preserving lines that current parser can't decode, so a future parser
109
+ * release still sees them.
110
+ */
111
+ export function appendJsonlLine(filePath: string, line: string): void {
112
+ mkdirSync(dirname(filePath), { recursive: true });
113
+ const lockPath = `${filePath}${LOCK_SUFFIX}`;
114
+ const payload = line.endsWith("\n") ? line : `${line}\n`;
115
+
116
+ let acquired = tryAcquireAppendLock(lockPath);
117
+ // Pre-check: if the existing lock is already stale (writer crashed before
118
+ // unlinking), reclaim it before sleeping. Saves the entire retry budget
119
+ // for the common stale-leftover case.
120
+ if (!acquired && isStaleLock(lockPath)) {
121
+ safeUnlink(lockPath);
122
+ acquired = tryAcquireAppendLock(lockPath);
123
+ }
124
+ if (!acquired) {
125
+ const start = Date.now();
126
+ while (!acquired && Date.now() - start < LOCK_TIMEOUT_MS) {
127
+ // Codex launches built-in stdio MCPs under Node+tsx while the runtime
128
+ // itself uses Bun. Atomics.wait gives both processes a blocking sleep
129
+ // without a CPU-spinning fallback or a Bun-only global.
130
+ sleepForAppendLock(LOCK_RETRY_MS);
131
+ acquired = tryAcquireAppendLock(lockPath);
132
+ // Recheck staleness inside the loop so a writer that crashes mid-wait
133
+ // doesn't leave us spinning the whole timeout.
134
+ if (!acquired && isStaleLock(lockPath)) {
135
+ safeUnlink(lockPath);
136
+ acquired = tryAcquireAppendLock(lockPath);
137
+ }
138
+ }
139
+ }
140
+
141
+ if (!acquired) {
142
+ // Best-effort fallback: append unlocked. Worst case the line interleaves
143
+ // with another writer and one of them is dropped at parse time — still
144
+ // better than dropping the entry on the floor here.
145
+ appendFileSync(filePath, payload);
146
+ return;
147
+ }
148
+
149
+ try {
150
+ appendFileSync(filePath, payload);
151
+ } finally {
152
+ safeUnlink(lockPath);
153
+ }
154
+ }
155
+
156
+ /** Overwrite a JSONL file with the given entries. Trailing newline included. */
157
+ export function writeJsonlFile(filePath: string, entries: readonly unknown[]): void {
158
+ const dir = dirname(filePath);
159
+ mkdirSync(dir, { recursive: true });
160
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random()
161
+ .toString(16)
162
+ .slice(2)}`;
163
+ const payload = `${entries.map((e) => JSON.stringify(e)).join("\n")}\n`;
164
+ let fd: number | null = null;
165
+ try {
166
+ fd = openSync(tmpPath, "w");
167
+ writeFileSync(fd, payload);
168
+ fsyncSync(fd);
169
+ closeSync(fd);
170
+ fd = null;
171
+ renameSync(tmpPath, filePath);
172
+ fsyncDirectoryBestEffort(dir);
173
+ } catch (err) {
174
+ if (fd !== null) {
175
+ try {
176
+ closeSync(fd);
177
+ } catch {}
178
+ }
179
+ try {
180
+ unlinkSync(tmpPath);
181
+ } catch {}
182
+ throw err;
183
+ }
184
+ }
185
+
186
+ function fsyncDirectoryBestEffort(dir: string): void {
187
+ let fd: number | null = null;
188
+ try {
189
+ fd = openSync(dir, "r");
190
+ fsyncSync(fd);
191
+ } catch {
192
+ // Directory fsync is not portable across every runtime/filesystem. The
193
+ // file fsync + rename above is the important integrity boundary; this is
194
+ // best-effort durability for the directory entry.
195
+ } finally {
196
+ if (fd !== null) {
197
+ try {
198
+ closeSync(fd);
199
+ } catch {}
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,174 @@
1
+ import { logger } from "#platform/logger";
2
+
3
+ /**
4
+ * Shared process-shutdown registry.
5
+ *
6
+ * The backend has multiple subsystems that need to clean up on SIGTERM /
7
+ * SIGINT / beforeExit — MCP client pool, browser/background-bash
8
+ * children, codex process trees, and DB/runtime handles. Before this module each subsystem
9
+ * registered its own `process.once("SIGTERM", ...)` handler. node fires
10
+ * them in registration order with no priority, so a cleanup that wanted
11
+ * to wait for in-flight MCP calls to finish could be racing the same
12
+ * subprocess-kill cleanup that takes those calls' children out from under
13
+ * it.
14
+ *
15
+ * Replace that with a single set of signal hooks + a priority-ordered
16
+ * registry. Modules call `onShutdown(name, priority, fn)`; the single
17
+ * handler iterates handlers in priority order (highest first), awaiting
18
+ * each with a hard ceiling so a hung cleanup can't keep the process
19
+ * alive past a reasonable bound.
20
+ *
21
+ * Priority convention (rough):
22
+ * - 100+: stateful network/connection resources whose in-flight calls
23
+ * deserve a graceful close before any subprocess gets killed
24
+ * (MCP pool, DB connections, WebSocket/server resources).
25
+ * - 50: subsystems that kill external processes / free OS handles
26
+ * (playwright, background-bash, codex tree).
27
+ * - 10: final reporting / metrics flushing.
28
+ * Numbers are not enforced — they're a convention so the order is
29
+ * scrutable at a glance.
30
+ *
31
+ * Idempotent: the first signal triggers; subsequent signals are
32
+ * no-ops so SIGINT-then-SIGTERM doesn't double-run handlers.
33
+ *
34
+ * Test isolation: `__resetForTests()` clears the registry AND removes
35
+ * the installed listeners so a fresh registration in the next test
36
+ * starts clean.
37
+ */
38
+
39
+ export interface ShutdownHandler {
40
+ name: string;
41
+ priority: number;
42
+ fn: () => Promise<void> | void;
43
+ }
44
+
45
+ const handlers: ShutdownHandler[] = [];
46
+ let signalHooksInstalled = false;
47
+ let triggered = false;
48
+ let shutdownPromise: Promise<void> | null = null;
49
+
50
+ /** Per-handler hard ceiling. A hung cleanup can't keep the process up
51
+ * beyond this; we log and move on so subsequent handlers still run. */
52
+ const HANDLER_TIMEOUT_MS = 5_000;
53
+
54
+ /** Process-exit-after-handlers ceiling. Even if every handler hangs we
55
+ * call process.exit so an orphaned bot doesn't wedge a forum forever. */
56
+ const HARD_EXIT_TIMEOUT_MS = 15_000;
57
+
58
+ /** Test-injectable signal trigger — exposed so tests can fire shutdown
59
+ * without touching real process signals. */
60
+ export type SignalReason = "beforeExit" | "SIGINT" | "SIGTERM" | "test";
61
+
62
+ /**
63
+ * Register a shutdown handler. Lazy-installs the single process-level
64
+ * signal listener on first call.
65
+ *
66
+ * `priority` is highest-first (100 runs before 50). Ties keep registration
67
+ * order (stable sort), so two subsystems at the same tier behave
68
+ * deterministically across runs.
69
+ */
70
+ export function onShutdown(name: string, priority: number, fn: () => Promise<void> | void): void {
71
+ handlers.push({ name, priority, fn });
72
+ ensureSignalHooks();
73
+ }
74
+
75
+ function ensureSignalHooks(): void {
76
+ if (signalHooksInstalled) return;
77
+ signalHooksInstalled = true;
78
+ process.once("beforeExit", () => {
79
+ void runShutdown("beforeExit");
80
+ });
81
+ process.once("SIGINT", () => {
82
+ void runShutdown("SIGINT");
83
+ });
84
+ process.once("SIGTERM", () => {
85
+ void runShutdown("SIGTERM");
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Run every registered handler in priority order. Each handler gets a
91
+ * HANDLER_TIMEOUT_MS budget; if it hangs we log and continue. The whole
92
+ * sweep is capped at HARD_EXIT_TIMEOUT_MS — past that we call
93
+ * `process.exit` so a stuck cleanup can't deadlock the shutdown.
94
+ *
95
+ * `reason` is passed through to handlers so a handler can log which
96
+ * signal triggered it (useful for debugging which path led to exit).
97
+ *
98
+ * Exported for tests / explicit "I want shutdown now" callers.
99
+ */
100
+ export function runShutdown(reason: SignalReason): Promise<void> {
101
+ if (shutdownPromise) return shutdownPromise;
102
+ triggered = true;
103
+ shutdownPromise = performShutdown(reason);
104
+ return shutdownPromise;
105
+ }
106
+
107
+ async function performShutdown(reason: SignalReason): Promise<void> {
108
+ logger.info({ reason, handlerCount: handlers.length }, "lifecycle: shutdown sequence starting");
109
+ // Stable sort: descending priority, registration order on ties.
110
+ const ordered = handlers
111
+ .map((h, i) => ({ h, i }))
112
+ .sort((a, b) => b.h.priority - a.h.priority || a.i - b.i)
113
+ .map(({ h }) => h);
114
+
115
+ const hardExit = setTimeout(() => {
116
+ logger.error({ reason }, "lifecycle: hard-exit ceiling reached, forcing process.exit");
117
+ process.exit(1);
118
+ }, HARD_EXIT_TIMEOUT_MS);
119
+ hardExit.unref?.();
120
+
121
+ for (const h of ordered) {
122
+ const start = Date.now();
123
+ let handlerTimeout: ReturnType<typeof setTimeout> | undefined;
124
+ try {
125
+ await Promise.race([
126
+ Promise.resolve().then(() => h.fn()),
127
+ new Promise<void>((_, reject) => {
128
+ handlerTimeout = setTimeout(
129
+ () => reject(new Error("handler timeout")),
130
+ HANDLER_TIMEOUT_MS,
131
+ );
132
+ }),
133
+ ]);
134
+ logger.info(
135
+ { handler: h.name, priority: h.priority, ms: Date.now() - start },
136
+ "lifecycle: shutdown handler completed",
137
+ );
138
+ } catch (err) {
139
+ logger.warn(
140
+ { err, handler: h.name, priority: h.priority, ms: Date.now() - start },
141
+ "lifecycle: shutdown handler failed or timed out (continuing)",
142
+ );
143
+ } finally {
144
+ if (handlerTimeout) clearTimeout(handlerTimeout);
145
+ }
146
+ }
147
+ clearTimeout(hardExit);
148
+ logger.info({ reason }, "lifecycle: shutdown sequence complete");
149
+ }
150
+
151
+ /** Test-only: clear the registry and uninstall signal hooks so the next
152
+ * test's onShutdown calls start fresh. Does NOT actually call any
153
+ * handler — tests that want to exercise the trigger path should call
154
+ * `runShutdown("test")` after registering. */
155
+ export function __resetForTests(): void {
156
+ handlers.length = 0;
157
+ triggered = false;
158
+ shutdownPromise = null;
159
+ signalHooksInstalled = false;
160
+ // No way to remove process.once listeners that haven't fired yet —
161
+ // they'll just become no-ops because `triggered` is false-but-handlers
162
+ // is empty. The signalHooksInstalled flag is reset so the next
163
+ // onShutdown call installs fresh listeners.
164
+ }
165
+
166
+ /** Test-only: read the registered handler count. */
167
+ export function __handlerCount(): number {
168
+ return handlers.length;
169
+ }
170
+
171
+ /** Test-only: read whether triggered. */
172
+ export function __triggered(): boolean {
173
+ return triggered;
174
+ }
@@ -0,0 +1,31 @@
1
+ import pino from "pino";
2
+
3
+ /**
4
+ * Always write logs to stderr (fd 2), never stdout.
5
+ *
6
+ * MCP servers under `src/mcp/**` run as stdio subprocesses where stdout is the
7
+ * JSON-RPC transport channel. A single log line on stdout corrupts the next
8
+ * message and the MCP client closes the transport ("Transport closed"). The
9
+ * main bot process is also fine with stderr — pm2 captures both streams.
10
+ *
11
+ * In dev mode pino spawns a `pino-pretty` worker that owns its own sink, so we
12
+ * pass `destination: 2` through transport.options. In prod (no transport) the
13
+ * second arg to `pino()` sets the destination directly.
14
+ */
15
+ export const logger = pino(
16
+ {
17
+ level: process.env.LOG_LEVEL || "info",
18
+ transport:
19
+ process.env.NODE_ENV === "development"
20
+ ? {
21
+ target: "pino-pretty",
22
+ options: {
23
+ colorize: true,
24
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss",
25
+ destination: 2,
26
+ },
27
+ }
28
+ : undefined,
29
+ },
30
+ pino.destination(2),
31
+ );
@@ -0,0 +1,3 @@
1
+ // The SDK's login-PATH bootstrap is expected host setup, not actionable
2
+ // runtime output. Keep it enabled while suppressing its module-load chatter.
3
+ process.env.MAESTRO_SDK_SILENT_BOOTSTRAP ??= "1";