negotium 0.1.7 → 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,347 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { SESSION_ASKS_DIR } from "#platform/config";
15
+
16
+ export const PENDING_ASK_TTL_MS = 15 * 60 * 1000;
17
+
18
+ export type PendingAskState =
19
+ | "requested"
20
+ | "reply_ready"
21
+ | "queued_for_caller"
22
+ | "injecting_to_caller";
23
+
24
+ export type PendingAskUserId = number | string;
25
+
26
+ export interface PendingAskRecord {
27
+ userId: PendingAskUserId;
28
+ from: string;
29
+ to: string;
30
+ requestId: string;
31
+ contextId?: string;
32
+ state: PendingAskState;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ }
36
+
37
+ export interface AskReplySource {
38
+ from?: string;
39
+ requestId?: string;
40
+ contextId?: string;
41
+ }
42
+
43
+ interface AskKey {
44
+ userId: PendingAskUserId;
45
+ from: string;
46
+ to: string;
47
+ }
48
+
49
+ interface AskIdentity extends AskKey {
50
+ requestId?: string;
51
+ }
52
+
53
+ function pendingAskDir(userId: PendingAskUserId): string {
54
+ return join(SESSION_ASKS_DIR, String(userId));
55
+ }
56
+
57
+ const ASK_FILENAME_PREFIX = "v2-";
58
+
59
+ function encodeAskKey(key: Pick<AskKey, "from" | "to">): string {
60
+ return Buffer.from(JSON.stringify([key.from, key.to]), "utf8").toString("base64url");
61
+ }
62
+
63
+ function pendingAskPath(key: AskKey): string {
64
+ return join(pendingAskDir(key.userId), `${ASK_FILENAME_PREFIX}${encodeAskKey(key)}.pending`);
65
+ }
66
+
67
+ function legacyPendingAskPath(key: AskKey): string | null {
68
+ if (key.from.includes("___") || /[\\/]/.test(key.from) || /[\\/]/.test(key.to)) return null;
69
+ return join(pendingAskDir(key.userId), `${key.from}___${key.to}.pending`);
70
+ }
71
+
72
+ function resolvePendingAskPath(key: AskKey): string {
73
+ const current = pendingAskPath(key);
74
+ if (existsSync(current)) return current;
75
+ const legacy = legacyPendingAskPath(key);
76
+ if (!legacy || !existsSync(legacy)) return current;
77
+
78
+ // Keep one durable identity across the filename transition. renameSync is
79
+ // atomic within this directory, so concurrent creators observe either the
80
+ // legacy request or its v2 name, never a copied half-state.
81
+ try {
82
+ renameSync(legacy, current);
83
+ return current;
84
+ } catch {
85
+ return existsSync(current) ? current : legacy;
86
+ }
87
+ }
88
+
89
+ function parsePendingAskFilename(fileName: string): { from: string; to: string } | null {
90
+ if (!fileName.endsWith(".pending")) return null;
91
+ const raw = fileName.slice(0, -".pending".length);
92
+ if (raw.startsWith(ASK_FILENAME_PREFIX)) {
93
+ try {
94
+ const decoded = JSON.parse(
95
+ Buffer.from(raw.slice(ASK_FILENAME_PREFIX.length), "base64url").toString("utf8"),
96
+ ) as unknown;
97
+ if (
98
+ Array.isArray(decoded) &&
99
+ decoded.length === 2 &&
100
+ decoded.every((part) => typeof part === "string")
101
+ ) {
102
+ return { from: decoded[0], to: decoded[1] };
103
+ }
104
+ } catch {
105
+ return null;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // Read files created before the opaque v2 key was introduced.
111
+ const sep = raw.indexOf("___");
112
+ if (sep < 0) return null;
113
+ return { from: raw.slice(0, sep), to: raw.slice(sep + 3) };
114
+ }
115
+
116
+ function readPendingAskFile(path: string, fallback: AskKey): PendingAskRecord | null {
117
+ try {
118
+ const raw = readFileSync(path, "utf8").trim();
119
+ if (!raw) return null;
120
+ if (!raw.startsWith("{")) {
121
+ const ts = new Date(statSync(path).mtimeMs).toISOString();
122
+ return {
123
+ userId: fallback.userId,
124
+ from: fallback.from,
125
+ to: fallback.to,
126
+ requestId: raw,
127
+ state: "requested",
128
+ createdAt: ts,
129
+ updatedAt: ts,
130
+ };
131
+ }
132
+ const parsed = JSON.parse(raw) as Partial<PendingAskRecord>;
133
+ if (!parsed.requestId || !parsed.from || !parsed.to) return null;
134
+ const now = new Date().toISOString();
135
+ return {
136
+ userId: parsed.userId ?? fallback.userId,
137
+ from: parsed.from,
138
+ to: parsed.to,
139
+ requestId: parsed.requestId,
140
+ contextId: parsed.contextId,
141
+ state: parsed.state ?? "requested",
142
+ createdAt: parsed.createdAt ?? now,
143
+ updatedAt: parsed.updatedAt ?? parsed.createdAt ?? now,
144
+ };
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function isStale(record: PendingAskRecord | null, path: string): boolean {
151
+ const ts = record ? Date.parse(record.updatedAt || record.createdAt) : Number.NaN;
152
+ if (Number.isFinite(ts)) return Date.now() - ts > PENDING_ASK_TTL_MS;
153
+ try {
154
+ return Date.now() - statSync(path).mtimeMs > PENDING_ASK_TTL_MS;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ function writePendingAsk(record: PendingAskRecord, path = pendingAskPath(record)): void {
161
+ mkdirSync(pendingAskDir(record.userId), { recursive: true });
162
+ writeFileSync(path, `${JSON.stringify(record)}\n`);
163
+ }
164
+
165
+ export function createPendingAsk(args: {
166
+ userId: PendingAskUserId;
167
+ from: string;
168
+ to: string;
169
+ requestId: string;
170
+ contextId?: string;
171
+ }):
172
+ | { ok: true; record: PendingAskRecord }
173
+ | { ok: false; existing: PendingAskRecord | null; stale: boolean } {
174
+ const path = resolvePendingAskPath(args);
175
+ const now = new Date().toISOString();
176
+ const record: PendingAskRecord = {
177
+ userId: args.userId,
178
+ from: args.from,
179
+ to: args.to,
180
+ requestId: args.requestId,
181
+ contextId: args.contextId,
182
+ state: "requested",
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ };
186
+
187
+ mkdirSync(pendingAskDir(args.userId), { recursive: true });
188
+ for (let attempt = 0; attempt < 2; attempt++) {
189
+ let fd: number | null = null;
190
+ try {
191
+ fd = openSync(path, "wx");
192
+ writeFileSync(fd, `${JSON.stringify(record)}\n`);
193
+ return { ok: true, record };
194
+ } catch (err) {
195
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
196
+ const existing = readPendingAskFile(path, args);
197
+ if (!isStale(existing, path)) return { ok: false, existing, stale: false };
198
+ try {
199
+ unlinkSync(path);
200
+ } catch {
201
+ return { ok: false, existing, stale: true };
202
+ }
203
+ } finally {
204
+ if (fd !== null) closeSync(fd);
205
+ }
206
+ }
207
+
208
+ return { ok: false, existing: readPendingAskFile(path, args), stale: true };
209
+ }
210
+
211
+ export function markPendingAskState(
212
+ args: AskIdentity & { state: PendingAskState },
213
+ ): PendingAskRecord | null {
214
+ const path = resolvePendingAskPath(args);
215
+ const record = readPendingAskFile(path, args);
216
+ if (!record) return null;
217
+ if (args.requestId && record.requestId !== args.requestId) return record;
218
+ const next = {
219
+ ...record,
220
+ state: args.state,
221
+ updatedAt: new Date().toISOString(),
222
+ };
223
+ writePendingAsk(next, path);
224
+ return next;
225
+ }
226
+
227
+ export function clearPendingAsk(args: AskIdentity): boolean {
228
+ const path = resolvePendingAskPath(args);
229
+ const record = readPendingAskFile(path, args);
230
+ if (args.requestId && record && record.requestId !== args.requestId) return false;
231
+ try {
232
+ unlinkSync(path);
233
+ return true;
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+
239
+ export function markPendingAskSources(args: {
240
+ userId: PendingAskUserId;
241
+ callerTopic: string;
242
+ sources?: AskReplySource[];
243
+ state: PendingAskState;
244
+ }): void {
245
+ for (const source of args.sources ?? []) {
246
+ if (!source.from) continue;
247
+ markPendingAskState({
248
+ userId: args.userId,
249
+ from: args.callerTopic,
250
+ to: source.from,
251
+ requestId: source.requestId,
252
+ state: args.state,
253
+ });
254
+ }
255
+ }
256
+
257
+ export function clearPendingAskSources(args: {
258
+ userId: PendingAskUserId;
259
+ callerTopic: string;
260
+ sources?: AskReplySource[];
261
+ }): void {
262
+ for (const source of args.sources ?? []) {
263
+ if (!source.from) continue;
264
+ clearPendingAsk({
265
+ userId: args.userId,
266
+ from: args.callerTopic,
267
+ to: source.from,
268
+ requestId: source.requestId,
269
+ });
270
+ }
271
+ }
272
+
273
+ export function listPendingAsksForCaller(args: {
274
+ userId: PendingAskUserId;
275
+ from: string;
276
+ }): PendingAskRecord[] {
277
+ const dir = pendingAskDir(args.userId);
278
+ let files: string[];
279
+ try {
280
+ files = readdirSync(dir);
281
+ } catch {
282
+ return [];
283
+ }
284
+
285
+ const records: PendingAskRecord[] = [];
286
+ for (const fileName of files) {
287
+ const parsed = parsePendingAskFilename(fileName);
288
+ if (!parsed || parsed.from !== args.from) continue;
289
+ const path = join(dir, fileName);
290
+ const record = readPendingAskFile(path, {
291
+ userId: args.userId,
292
+ from: parsed.from,
293
+ to: parsed.to,
294
+ });
295
+ if (!record) continue;
296
+ if (isStale(record, path)) {
297
+ try {
298
+ unlinkSync(path);
299
+ } catch {
300
+ // Best-effort stale cleanup only.
301
+ }
302
+ continue;
303
+ }
304
+ records.push(record);
305
+ }
306
+ return records;
307
+ }
308
+
309
+ /** Remove durable ask edges to or from a topic that no longer exists. */
310
+ export function deletePendingAsksForTopic(args: {
311
+ userId: PendingAskUserId;
312
+ topicName: string;
313
+ }): number {
314
+ const dir = pendingAskDir(args.userId);
315
+ let files: string[];
316
+ try {
317
+ files = readdirSync(dir);
318
+ } catch {
319
+ return 0;
320
+ }
321
+
322
+ let deleted = 0;
323
+ for (const fileName of files) {
324
+ const parsed = parsePendingAskFilename(fileName);
325
+ if (!parsed || (parsed.from !== args.topicName && parsed.to !== args.topicName)) continue;
326
+ try {
327
+ unlinkSync(join(dir, fileName));
328
+ deleted++;
329
+ } catch {
330
+ // Best-effort teardown; stale cleanup will retry any file that survives.
331
+ }
332
+ }
333
+ return deleted;
334
+ }
335
+
336
+ export function describePendingAskState(state: PendingAskState): string {
337
+ switch (state) {
338
+ case "requested":
339
+ return "대상 세션 처리 중";
340
+ case "reply_ready":
341
+ return "응답 생성 완료, 이 세션 주입 준비 중";
342
+ case "queued_for_caller":
343
+ return "응답 생성 완료, 이 세션 실행 종료 후 주입 대기 중";
344
+ case "injecting_to_caller":
345
+ return "응답을 이 세션에 주입 중";
346
+ }
347
+ }
@@ -0,0 +1,129 @@
1
+ // Runtime-adaptive SQLite façade.
2
+ //
3
+ // Why this exists: Otium normally runs on bun and uses `bun:sqlite` (native,
4
+ // fast, zero-build). But the `codex` agent's MCP servers must run on pure node
5
+ // — codex 0.135's rmcp stdio client can't handshake with bun-spawned servers
6
+ // (see serverLaunch in platform/mcp-config.ts). `bun:sqlite` doesn't exist
7
+ // under node, so any MCP server that touches the DB would crash on import.
8
+ //
9
+ // This module picks the backend at runtime and re-exports it as `Database`:
10
+ // - under bun → the real `bun:sqlite` Database (unchanged behavior)
11
+ // - under node → a thin shim over node:sqlite's DatabaseSync exposing the
12
+ // exact subset Otium uses: new Database(path, {readonly?,create?}),
13
+ // .query()/.prepare() → statement, .exec(), .run(), .transaction(fn),
14
+ // .close(); statements support positional `?` params via .run()/.get()/
15
+ // .all(). (No named-object params / .values()/.iterate()/.as() are used in
16
+ // this codebase, so the shim intentionally omits them.)
17
+ //
18
+ // The TYPE of the exported `Database` is bun:sqlite's own class type (imported
19
+ // type-only, so it's erased at runtime and never resolved under node). That
20
+ // keeps every existing call site — including generic `.query<Row, Params>()`
21
+ // usages — type-checking exactly as before with zero changes.
22
+ //
23
+ // The bun branch uses a dynamic import so node never resolves the `bun:sqlite`
24
+ // specifier (dead code under node), and the node branch's `node:sqlite` import
25
+ // never runs under bun.
26
+
27
+ import type { Database as BunDatabase } from "bun:sqlite";
28
+
29
+ // `mock.module()` can replace the Bun global while a test graph is being
30
+ // evaluated. The runtime version flag is stable and is also the documented
31
+ // cross-runtime discriminator.
32
+ const isBun = typeof process.versions.bun === "string";
33
+
34
+ type DatabaseCtor = typeof BunDatabase;
35
+
36
+ let Database: DatabaseCtor;
37
+
38
+ if (isBun) {
39
+ ({ Database } = await import("bun:sqlite"));
40
+ } else {
41
+ // node:sqlite has no type declarations under our tsconfig (`types: bun-types`
42
+ // only), and `bun-types` doesn't ship them — so type the dynamic import
43
+ // locally and use a `string` specifier to keep tsc from trying to resolve it.
44
+ type NodeStatement = {
45
+ run(...params: unknown[]): unknown;
46
+ get(...params: unknown[]): unknown;
47
+ all(...params: unknown[]): unknown[];
48
+ };
49
+ type NodeDatabaseSync = {
50
+ prepare(sql: string): NodeStatement;
51
+ exec(sql: string): void;
52
+ close(): void;
53
+ };
54
+ type NodeSqliteModule = {
55
+ DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => NodeDatabaseSync;
56
+ };
57
+ // Keep Bun's resolver from eagerly resolving the Node-only builtin even
58
+ // though this branch is dead there (notably under `bun test` + mock.module).
59
+ const nodeSqliteSpecifier = ["node", "sqlite"].join(":");
60
+ const { DatabaseSync } = (await import(nodeSqliteSpecifier)) as NodeSqliteModule;
61
+
62
+ type DbOptions = { readonly?: boolean; create?: boolean };
63
+
64
+ class NodeDatabase {
65
+ #db: NodeDatabaseSync;
66
+
67
+ constructor(path: string, options: DbOptions = {}) {
68
+ // node:sqlite opens read-write and creates-if-missing by default, which
69
+ // matches bun's `{create:true}`. For readonly we must not create.
70
+ this.#db = options.readonly
71
+ ? new DatabaseSync(path, { readOnly: true })
72
+ : new DatabaseSync(path);
73
+ }
74
+
75
+ query(sql: string) {
76
+ return this.#db.prepare(sql);
77
+ }
78
+
79
+ prepare(sql: string) {
80
+ return this.#db.prepare(sql);
81
+ }
82
+
83
+ exec(sql: string): void {
84
+ this.#db.exec(sql);
85
+ }
86
+
87
+ run(sql: string, ...params: unknown[]) {
88
+ return this.#db.prepare(sql).run(...params);
89
+ }
90
+
91
+ transaction<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R {
92
+ // bun's db.transaction(fn) returns a function that runs fn inside a
93
+ // transaction when called. node:sqlite has no helper, so emulate with
94
+ // BEGIN/COMMIT/ROLLBACK. It also exposes deferred/immediate/exclusive
95
+ // variants; mirror those so Node-launched MCP servers can share call
96
+ // sites with Bun.
97
+ const run =
98
+ (begin: "BEGIN" | "BEGIN DEFERRED" | "BEGIN IMMEDIATE" | "BEGIN EXCLUSIVE") =>
99
+ (...args: Args): R => {
100
+ this.#db.exec(begin);
101
+ try {
102
+ const result = fn(...args);
103
+ this.#db.exec("COMMIT");
104
+ return result;
105
+ } catch (err) {
106
+ this.#db.exec("ROLLBACK");
107
+ throw err;
108
+ }
109
+ };
110
+ const tx = run("BEGIN") as ((...args: Args) => R) & {
111
+ deferred: (...args: Args) => R;
112
+ immediate: (...args: Args) => R;
113
+ exclusive: (...args: Args) => R;
114
+ };
115
+ tx.deferred = run("BEGIN DEFERRED");
116
+ tx.immediate = run("BEGIN IMMEDIATE");
117
+ tx.exclusive = run("BEGIN EXCLUSIVE");
118
+ return tx;
119
+ }
120
+
121
+ close(): void {
122
+ this.#db.close();
123
+ }
124
+ }
125
+
126
+ Database = NodeDatabase as unknown as DatabaseCtor;
127
+ }
128
+
129
+ export { Database };
@@ -0,0 +1,179 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { DATA_DIR } from "#platform/config";
4
+ import { logger } from "#platform/logger";
5
+ import { sanitizeFileName } from "#security/sanitize";
6
+ import type { TaskSnapshot } from "#types";
7
+
8
+ /**
9
+ * Otium-owned task store, shared by every agent backend.
10
+ *
11
+ * Provider-native task/todo stores are not authoritative because they live
12
+ * inside one SDK session. This store is keyed by Otium topic context so task
13
+ * state survives agent switches and feeds the same live task panel for
14
+ * claude/codex/maestro.
15
+ */
16
+ export type StoredTask = TaskSnapshot;
17
+
18
+ export const TASK_STATUS_VALUES = ["pending", "in_progress", "completed"] as const;
19
+
20
+ interface TaskFileShape {
21
+ version: 1;
22
+ tasks: StoredTask[];
23
+ }
24
+
25
+ function safeUserIdComponent(userId: number | string): string {
26
+ const str = String(userId);
27
+ if (!str || /[/\\]|\.\./.test(str)) {
28
+ throw new Error(`tasks: refusing unsafe userId path component: ${str}`);
29
+ }
30
+ return str;
31
+ }
32
+
33
+ function safeTaskScopeKey(scopeKey: string): string {
34
+ const safe = sanitizeFileName(scopeKey);
35
+ if (!safe || safe === "." || safe === "..") {
36
+ throw new Error(`tasks: refusing unsafe scope key: ${scopeKey}`);
37
+ }
38
+ return safe;
39
+ }
40
+
41
+ export function taskScopeKey(opts: { topicId?: string; session: string }): string {
42
+ return opts.topicId?.trim() || opts.session || "default";
43
+ }
44
+
45
+ export function getTaskFilePath(userId: number | string, scopeKey: string): string {
46
+ return join(DATA_DIR, "tasks", safeUserIdComponent(userId), `${safeTaskScopeKey(scopeKey)}.json`);
47
+ }
48
+
49
+ export function readTasks(userId: number | string, scopeKey: string): StoredTask[] {
50
+ const path = getTaskFilePath(userId, scopeKey);
51
+ if (!existsSync(path)) return [];
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as TaskFileShape;
54
+ return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
55
+ } catch (e) {
56
+ logger.warn({ err: e, path }, "tasks: failed to read task store");
57
+ return [];
58
+ }
59
+ }
60
+
61
+ export function writeTasks(userId: number | string, scopeKey: string, tasks: StoredTask[]): void {
62
+ const path = getTaskFilePath(userId, scopeKey);
63
+ mkdirSync(dirname(path), { recursive: true });
64
+ const payload: TaskFileShape = { version: 1, tasks };
65
+ const tmp = `${path}.${process.pid}.tmp`;
66
+ writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8");
67
+ renameSync(tmp, path);
68
+ }
69
+
70
+ export function taskFileMtimeNs(userId: number | string, scopeKey: string): bigint | null {
71
+ try {
72
+ return statSync(getTaskFilePath(userId, scopeKey), { bigint: true }).mtimeNs;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export interface TaskCreateInput {
79
+ subject: string;
80
+ activeForm?: string;
81
+ blockedBy?: string[];
82
+ owner?: string;
83
+ }
84
+
85
+ export interface TaskUpdateInput {
86
+ id: string;
87
+ status?: StoredTask["status"];
88
+ subject?: string;
89
+ activeForm?: string;
90
+ blockedBy?: string[];
91
+ owner?: string;
92
+ }
93
+
94
+ function nextTaskId(tasks: StoredTask[]): number {
95
+ let max = 0;
96
+ for (const task of tasks) {
97
+ const n = Number(task.id);
98
+ if (Number.isInteger(n) && n > max) max = n;
99
+ }
100
+ return max + 1;
101
+ }
102
+
103
+ export function createTasks(
104
+ tasks: StoredTask[],
105
+ inputs: TaskCreateInput[],
106
+ ): { tasks: StoredTask[]; created: StoredTask[] } {
107
+ const out = [...tasks];
108
+ const created: StoredTask[] = [];
109
+ let id = nextTaskId(out);
110
+ for (const input of inputs) {
111
+ const task: StoredTask = {
112
+ id: String(id++),
113
+ subject: input.subject,
114
+ status: "pending",
115
+ ...(input.blockedBy && input.blockedBy.length > 0 ? { blockedBy: input.blockedBy } : {}),
116
+ ...(input.activeForm ? { activeForm: input.activeForm } : {}),
117
+ ...(input.owner ? { owner: input.owner } : {}),
118
+ };
119
+ out.push(task);
120
+ created.push(task);
121
+ }
122
+ return { tasks: out, created };
123
+ }
124
+
125
+ export function updateTasks(
126
+ tasks: StoredTask[],
127
+ updates: TaskUpdateInput[],
128
+ ): { tasks: StoredTask[]; missing: string[] } {
129
+ const out = tasks.map((task) => ({ ...task }));
130
+ const byId = new Map(out.map((task) => [task.id, task]));
131
+ const missing: string[] = [];
132
+ for (const update of updates) {
133
+ const task = byId.get(update.id);
134
+ if (!task) {
135
+ missing.push(update.id);
136
+ continue;
137
+ }
138
+ if (update.status !== undefined) task.status = update.status;
139
+ if (update.subject !== undefined) task.subject = update.subject;
140
+ if (update.activeForm !== undefined) {
141
+ if (update.activeForm) task.activeForm = update.activeForm;
142
+ else delete task.activeForm;
143
+ }
144
+ if (update.owner !== undefined) {
145
+ if (update.owner) task.owner = update.owner;
146
+ else delete task.owner;
147
+ }
148
+ if (update.blockedBy !== undefined) {
149
+ if (update.blockedBy.length > 0) task.blockedBy = update.blockedBy;
150
+ else delete task.blockedBy;
151
+ }
152
+ }
153
+ return { tasks: out, missing };
154
+ }
155
+
156
+ export function deleteTasks(
157
+ tasks: StoredTask[],
158
+ opts: { ids?: string[]; all?: boolean },
159
+ ): { tasks: StoredTask[]; removed: number } {
160
+ if (opts.all) return { tasks: [], removed: tasks.length };
161
+ const ids = new Set(opts.ids ?? []);
162
+ const kept = tasks.filter((task) => !ids.has(task.id));
163
+ return { tasks: kept, removed: tasks.length - kept.length };
164
+ }
165
+
166
+ export function renderTaskList(tasks: StoredTask[]): string {
167
+ if (tasks.length === 0) return "작업 목록이 비어 있습니다.";
168
+ const mark = { pending: "[ ]", in_progress: "[->]", completed: "[x]" } as const;
169
+ const lines = tasks.map((task) => {
170
+ const deps =
171
+ task.blockedBy && task.blockedBy.length > 0
172
+ ? ` (blocked by #${task.blockedBy.join(", #")})`
173
+ : "";
174
+ const owner = task.owner ? ` @${task.owner}` : "";
175
+ return `${mark[task.status]} #${task.id} ${task.subject}${deps}${owner}`;
176
+ });
177
+ const done = tasks.filter((task) => task.status === "completed").length;
178
+ return [`Tasks (${done}/${tasks.length} 완료)`, ...lines].join("\n");
179
+ }