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,301 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { db } from "#storage/forum-db";
3
+ import { TURN_LEASE_STALE_MS } from "#storage/runtime-leases";
4
+ import { getRuntimeTopicEpoch, TOPIC_MAINTENANCE_STALE_MS } from "#storage/runtime-topic-state";
5
+ import type { AgentKind, EffortLevel, PeerRuntimeBridgeContext } from "#types";
6
+
7
+ const REQUEST_CLAIM_STALE_MS = TURN_LEASE_STALE_MS;
8
+
9
+ /** Serializable execution details that must survive a cross-process handoff. */
10
+ export interface RuntimeUserTurnExecution {
11
+ runtimeEpoch?: number;
12
+ sourceRequestId?: string;
13
+ agentOverride?: AgentKind;
14
+ modelOverride?: string;
15
+ effortOverride?: EffortLevel;
16
+ sessionId?: string | null;
17
+ sessionIdSpecified?: boolean;
18
+ sessionScope?: "topic" | "isolated";
19
+ cwd?: string;
20
+ sessionName?: string;
21
+ sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron";
22
+ visualTools?: boolean;
23
+ bridgeSessionFromHistory?: boolean;
24
+ peerBridge?: PeerRuntimeBridgeContext;
25
+ from?: string;
26
+ }
27
+
28
+ export interface RuntimeUserTurnRequest {
29
+ requestId: string;
30
+ topicId: string;
31
+ userId: string;
32
+ prompt: string;
33
+ attachments?: string[];
34
+ allowAutoContinue: boolean;
35
+ execution?: RuntimeUserTurnExecution;
36
+ topicEpoch: number;
37
+ createdAt: number;
38
+ status: "pending" | "running";
39
+ claimedBy?: string;
40
+ claimedAt?: number;
41
+ runningQueryId?: string;
42
+ }
43
+
44
+ interface RuntimeUserTurnRequestRow {
45
+ request_id: string;
46
+ topic_id: string;
47
+ user_id: string;
48
+ prompt: string;
49
+ attachments_json: string | null;
50
+ allow_auto_continue: number;
51
+ execution_json: string | null;
52
+ topic_epoch: number | bigint;
53
+ created_at: number | bigint;
54
+ status: string;
55
+ claimed_by: string | null;
56
+ claimed_at: number | bigint | null;
57
+ running_query_id: string | null;
58
+ }
59
+
60
+ db.exec(`
61
+ CREATE TABLE IF NOT EXISTS runtime_user_turn_requests (
62
+ topic_id TEXT PRIMARY KEY,
63
+ request_id TEXT NOT NULL UNIQUE,
64
+ user_id TEXT NOT NULL,
65
+ prompt TEXT NOT NULL,
66
+ attachments_json TEXT,
67
+ allow_auto_continue INTEGER NOT NULL DEFAULT 1 CHECK (allow_auto_continue IN (0, 1)),
68
+ execution_json TEXT,
69
+ topic_epoch INTEGER NOT NULL DEFAULT 0,
70
+ created_at INTEGER NOT NULL,
71
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running')),
72
+ claimed_by TEXT,
73
+ claimed_at INTEGER,
74
+ running_query_id TEXT
75
+ )
76
+ `);
77
+ try {
78
+ db.exec("ALTER TABLE runtime_user_turn_requests ADD COLUMN execution_json TEXT");
79
+ } catch {
80
+ // Existing standalone database already has the additive handoff column.
81
+ }
82
+ try {
83
+ db.exec(
84
+ "ALTER TABLE runtime_user_turn_requests ADD COLUMN topic_epoch INTEGER NOT NULL DEFAULT 0",
85
+ );
86
+ } catch {
87
+ // Existing standalone database already has the additive epoch column.
88
+ }
89
+ db.exec(
90
+ "CREATE INDEX IF NOT EXISTS idx_runtime_user_turn_requests_ready ON runtime_user_turn_requests(status, created_at)",
91
+ );
92
+
93
+ function rowToRequest(row: RuntimeUserTurnRequestRow): RuntimeUserTurnRequest {
94
+ let attachments: string[] | undefined;
95
+ if (row.attachments_json) {
96
+ try {
97
+ const parsed = JSON.parse(row.attachments_json) as unknown;
98
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
99
+ attachments = parsed;
100
+ }
101
+ } catch {
102
+ attachments = undefined;
103
+ }
104
+ }
105
+ let execution: RuntimeUserTurnExecution | undefined;
106
+ if (row.execution_json) {
107
+ try {
108
+ const parsed = JSON.parse(row.execution_json) as unknown;
109
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
110
+ execution = parsed as RuntimeUserTurnExecution;
111
+ }
112
+ } catch {
113
+ execution = undefined;
114
+ }
115
+ }
116
+ return {
117
+ requestId: row.request_id,
118
+ topicId: row.topic_id,
119
+ userId: row.user_id,
120
+ prompt: row.prompt,
121
+ attachments,
122
+ allowAutoContinue: row.allow_auto_continue !== 0,
123
+ execution,
124
+ topicEpoch: Number(row.topic_epoch),
125
+ createdAt: Number(row.created_at),
126
+ status: row.status === "running" ? "running" : "pending",
127
+ claimedBy: row.claimed_by ?? undefined,
128
+ claimedAt: row.claimed_at === null ? undefined : Number(row.claimed_at),
129
+ runningQueryId: row.running_query_id ?? undefined,
130
+ };
131
+ }
132
+
133
+ export function enqueueRuntimeUserTurnRequest(input: {
134
+ topicId: string;
135
+ userId: string;
136
+ prompt: string;
137
+ attachments?: string[];
138
+ allowAutoContinue: boolean;
139
+ requestId?: string;
140
+ execution?: RuntimeUserTurnExecution;
141
+ topicEpoch?: number;
142
+ }): string {
143
+ const requestId = input.requestId ?? randomUUID();
144
+ const now = Date.now();
145
+ const topicEpoch = input.topicEpoch ?? getRuntimeTopicEpoch(input.topicId);
146
+ db.query(
147
+ `INSERT INTO runtime_user_turn_requests
148
+ (topic_id, request_id, user_id, prompt, attachments_json,
149
+ allow_auto_continue, execution_json, topic_epoch, created_at,
150
+ status, claimed_by, claimed_at, running_query_id)
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL)
152
+ ON CONFLICT(topic_id) DO UPDATE SET
153
+ request_id = excluded.request_id,
154
+ user_id = excluded.user_id,
155
+ prompt = excluded.prompt,
156
+ attachments_json = excluded.attachments_json,
157
+ allow_auto_continue = excluded.allow_auto_continue,
158
+ execution_json = excluded.execution_json,
159
+ topic_epoch = excluded.topic_epoch,
160
+ created_at = excluded.created_at,
161
+ status = 'pending',
162
+ claimed_by = NULL,
163
+ claimed_at = NULL,
164
+ running_query_id = NULL`,
165
+ ).run(
166
+ input.topicId,
167
+ requestId,
168
+ input.userId,
169
+ input.prompt,
170
+ input.attachments?.length ? JSON.stringify(input.attachments) : null,
171
+ input.allowAutoContinue ? 1 : 0,
172
+ input.execution ? JSON.stringify(input.execution) : null,
173
+ topicEpoch,
174
+ now,
175
+ );
176
+ return requestId;
177
+ }
178
+
179
+ export function claimNextRuntimeUserTurnRequest(
180
+ ownerId: string,
181
+ now = Date.now(),
182
+ ): RuntimeUserTurnRequest | null {
183
+ return db
184
+ .transaction(() => {
185
+ const row = db
186
+ .query<RuntimeUserTurnRequestRow, [number, number, number, number]>(
187
+ `SELECT r.*
188
+ FROM runtime_user_turn_requests r
189
+ LEFT JOIN runtime_turn_leases l ON l.topic_id = r.topic_id
190
+ LEFT JOIN runtime_topic_state s ON s.topic_id = r.topic_id
191
+ WHERE (l.topic_id IS NULL OR l.heartbeat_at < ?)
192
+ AND (s.topic_id IS NULL OR s.maintenance = 0 OR s.heartbeat_at < ?)
193
+ AND r.topic_epoch = COALESCE(s.epoch, 0)
194
+ AND (
195
+ (r.status = 'pending' AND (r.claimed_at IS NULL OR r.claimed_at < ?))
196
+ OR (r.status = 'running' AND (r.claimed_at IS NULL OR r.claimed_at < ?))
197
+ )
198
+ ORDER BY r.created_at ASC
199
+ LIMIT 1`,
200
+ )
201
+ .get(
202
+ now - TURN_LEASE_STALE_MS,
203
+ now - TOPIC_MAINTENANCE_STALE_MS,
204
+ now - REQUEST_CLAIM_STALE_MS,
205
+ now - REQUEST_CLAIM_STALE_MS,
206
+ );
207
+ if (!row) return null;
208
+ const updated = db
209
+ .query(
210
+ `UPDATE runtime_user_turn_requests
211
+ SET claimed_by = ?, claimed_at = ?
212
+ WHERE topic_id = ? AND request_id = ?
213
+ AND (
214
+ (status = 'pending' AND (claimed_at IS NULL OR claimed_at < ?))
215
+ OR (status = 'running' AND (claimed_at IS NULL OR claimed_at < ?))
216
+ )`,
217
+ )
218
+ .run(
219
+ ownerId,
220
+ now,
221
+ row.topic_id,
222
+ row.request_id,
223
+ now - REQUEST_CLAIM_STALE_MS,
224
+ now - REQUEST_CLAIM_STALE_MS,
225
+ );
226
+ if (Number(updated.changes ?? 0) === 0) return null;
227
+ return rowToRequest({ ...row, claimed_by: ownerId, claimed_at: now });
228
+ })
229
+ .immediate();
230
+ }
231
+
232
+ export function markRuntimeUserTurnRunning(
233
+ topicId: string,
234
+ requestId: string,
235
+ ownerId: string,
236
+ queryId: string,
237
+ ): boolean {
238
+ const result = db
239
+ .query(
240
+ `UPDATE runtime_user_turn_requests
241
+ SET status = 'running', running_query_id = ?, claimed_at = ?
242
+ WHERE topic_id = ? AND request_id = ? AND claimed_by = ?`,
243
+ )
244
+ .run(queryId, Date.now(), topicId, requestId, ownerId);
245
+ return Number(result.changes ?? 0) > 0;
246
+ }
247
+
248
+ export function releaseRuntimeUserTurnClaim(
249
+ topicId: string,
250
+ requestId: string,
251
+ ownerId: string,
252
+ ): boolean {
253
+ const result = db
254
+ .query(
255
+ `UPDATE runtime_user_turn_requests
256
+ SET status = 'pending', claimed_by = NULL, claimed_at = NULL, running_query_id = NULL
257
+ WHERE topic_id = ? AND request_id = ? AND claimed_by = ?`,
258
+ )
259
+ .run(topicId, requestId, ownerId);
260
+ return Number(result.changes ?? 0) > 0;
261
+ }
262
+
263
+ export function completeRuntimeUserTurnRequest(topicId: string, requestId: string): boolean {
264
+ const result = db
265
+ .query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ? AND request_id = ?")
266
+ .run(topicId, requestId);
267
+ return Number(result.changes ?? 0) > 0;
268
+ }
269
+
270
+ /** Cancel work captured before a reset/delete epoch began. */
271
+ export function cancelRuntimeUserTurnRequestsBeforeEpoch(topicId: string, epoch: number): string[] {
272
+ const rows = db
273
+ .query<{ request_id: string }, [string, number]>(
274
+ "SELECT request_id FROM runtime_user_turn_requests WHERE topic_id = ? AND topic_epoch < ?",
275
+ )
276
+ .all(topicId, epoch);
277
+ db.query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ? AND topic_epoch < ?").run(
278
+ topicId,
279
+ epoch,
280
+ );
281
+ return rows.map((row) => row.request_id);
282
+ }
283
+
284
+ export function cancelRuntimeUserTurnRequests(topicId: string): string[] {
285
+ const rows = db
286
+ .query<{ request_id: string }, [string]>(
287
+ "SELECT request_id FROM runtime_user_turn_requests WHERE topic_id = ?",
288
+ )
289
+ .all(topicId);
290
+ db.query("DELETE FROM runtime_user_turn_requests WHERE topic_id = ?").run(topicId);
291
+ return rows.map((row) => row.request_id);
292
+ }
293
+
294
+ export function getRuntimeUserTurnRequest(topicId: string): RuntimeUserTurnRequest | null {
295
+ const row = db
296
+ .query<RuntimeUserTurnRequestRow, [string]>(
297
+ "SELECT * FROM runtime_user_turn_requests WHERE topic_id = ?",
298
+ )
299
+ .get(topicId);
300
+ return row ? rowToRequest(row) : null;
301
+ }
@@ -0,0 +1,298 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { db } from "#storage/forum-db";
3
+ import { TURN_LEASE_STALE_MS } from "#storage/runtime-leases";
4
+ import { TOPIC_MAINTENANCE_STALE_MS } from "#storage/runtime-topic-state";
5
+
6
+ export type SelfScheduleStatus = "pending" | "running";
7
+
8
+ export interface SelfSchedule {
9
+ id: string;
10
+ topicId: string;
11
+ userId: string;
12
+ message: string;
13
+ deliverAt: number;
14
+ createdAt: number;
15
+ updatedAt: number;
16
+ status: SelfScheduleStatus;
17
+ claimedBy?: string;
18
+ claimedAt?: number;
19
+ runningQueryId?: string;
20
+ }
21
+
22
+ interface SelfScheduleRow {
23
+ id: string;
24
+ topic_id: string;
25
+ user_id: string;
26
+ message: string;
27
+ deliver_at: number | bigint;
28
+ created_at: number | bigint;
29
+ updated_at: number | bigint;
30
+ status: string;
31
+ claimed_by: string | null;
32
+ claimed_at: number | bigint | null;
33
+ running_query_id: string | null;
34
+ }
35
+
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS runtime_self_schedules (
38
+ id TEXT PRIMARY KEY,
39
+ topic_id TEXT NOT NULL,
40
+ user_id TEXT NOT NULL,
41
+ message TEXT NOT NULL,
42
+ deliver_at INTEGER NOT NULL,
43
+ created_at INTEGER NOT NULL,
44
+ updated_at INTEGER NOT NULL,
45
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running')),
46
+ claimed_by TEXT,
47
+ claimed_at INTEGER,
48
+ running_query_id TEXT
49
+ )
50
+ `);
51
+ db.exec(`
52
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_runtime_self_schedules_one_pending
53
+ ON runtime_self_schedules(topic_id)
54
+ WHERE status = 'pending'
55
+ `);
56
+ db.exec(`
57
+ CREATE INDEX IF NOT EXISTS idx_runtime_self_schedules_due
58
+ ON runtime_self_schedules(status, deliver_at)
59
+ `);
60
+
61
+ function rowToSchedule(row: SelfScheduleRow): SelfSchedule {
62
+ return {
63
+ id: row.id,
64
+ topicId: row.topic_id,
65
+ userId: row.user_id,
66
+ message: row.message,
67
+ deliverAt: Number(row.deliver_at),
68
+ createdAt: Number(row.created_at),
69
+ updatedAt: Number(row.updated_at),
70
+ status: row.status === "running" ? "running" : "pending",
71
+ claimedBy: row.claimed_by ?? undefined,
72
+ claimedAt: row.claimed_at === null ? undefined : Number(row.claimed_at),
73
+ runningQueryId: row.running_query_id ?? undefined,
74
+ };
75
+ }
76
+
77
+ export type CreatePendingSelfScheduleResult =
78
+ | { ok: true; schedule: SelfSchedule }
79
+ | { ok: false; existing: SelfSchedule };
80
+
81
+ /** Create the topic's sole pending self-schedule without replacing an existing one. */
82
+ export function createPendingSelfSchedule(input: {
83
+ topicId: string;
84
+ userId: string;
85
+ message: string;
86
+ deliverAt: number;
87
+ now?: number;
88
+ }): CreatePendingSelfScheduleResult {
89
+ return db
90
+ .transaction(() => {
91
+ const existing = getPendingSelfSchedule(input.topicId);
92
+ if (existing) return { ok: false as const, existing };
93
+
94
+ const now = input.now ?? Date.now();
95
+ const schedule: SelfSchedule = {
96
+ id: `scheduled-${randomUUID()}`,
97
+ topicId: input.topicId,
98
+ userId: input.userId,
99
+ message: input.message,
100
+ deliverAt: input.deliverAt,
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ status: "pending",
104
+ };
105
+ db.query(
106
+ `INSERT INTO runtime_self_schedules
107
+ (id, topic_id, user_id, message, deliver_at, created_at, updated_at, status)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')`,
109
+ ).run(
110
+ schedule.id,
111
+ schedule.topicId,
112
+ schedule.userId,
113
+ schedule.message,
114
+ schedule.deliverAt,
115
+ schedule.createdAt,
116
+ schedule.updatedAt,
117
+ );
118
+ return { ok: true as const, schedule };
119
+ })
120
+ .immediate();
121
+ }
122
+
123
+ export function getPendingSelfSchedule(topicId: string): SelfSchedule | null {
124
+ const row = db
125
+ .query<SelfScheduleRow, [string]>(
126
+ "SELECT * FROM runtime_self_schedules WHERE topic_id = ? AND status = 'pending'",
127
+ )
128
+ .get(topicId);
129
+ return row ? rowToSchedule(row) : null;
130
+ }
131
+
132
+ export function updatePendingSelfSchedule(input: {
133
+ topicId: string;
134
+ scheduleId: string;
135
+ message?: string;
136
+ deliverAt?: number;
137
+ now?: number;
138
+ }): SelfSchedule | null {
139
+ const existing = getPendingSelfSchedule(input.topicId);
140
+ if (!existing || existing.id !== input.scheduleId) return null;
141
+ const result = db
142
+ .query(
143
+ `UPDATE runtime_self_schedules
144
+ SET message = ?, deliver_at = ?, updated_at = ?
145
+ WHERE id = ? AND topic_id = ? AND status = 'pending'`,
146
+ )
147
+ .run(
148
+ input.message ?? existing.message,
149
+ input.deliverAt ?? existing.deliverAt,
150
+ input.now ?? Date.now(),
151
+ input.scheduleId,
152
+ input.topicId,
153
+ );
154
+ return Number(result.changes ?? 0) > 0 ? getPendingSelfSchedule(input.topicId) : null;
155
+ }
156
+
157
+ export function cancelPendingSelfSchedule(topicId: string, scheduleId: string): boolean {
158
+ const result = db
159
+ .query(
160
+ "DELETE FROM runtime_self_schedules WHERE id = ? AND topic_id = ? AND status = 'pending'",
161
+ )
162
+ .run(scheduleId, topicId);
163
+ return Number(result.changes ?? 0) > 0;
164
+ }
165
+
166
+ /**
167
+ * Claim one due schedule whose topic is idle. A stale running row is retryable
168
+ * after its process and topic lease disappear, preserving work across crashes.
169
+ */
170
+ export function claimNextDueSelfSchedule(ownerId: string, now = Date.now()): SelfSchedule | null {
171
+ const staleBefore = now - TURN_LEASE_STALE_MS;
172
+ return db
173
+ .transaction(() => {
174
+ const row = db
175
+ .query<SelfScheduleRow, [number, number, number, number]>(
176
+ `SELECT s.*
177
+ FROM runtime_self_schedules s
178
+ LEFT JOIN runtime_turn_leases l ON l.topic_id = s.topic_id
179
+ LEFT JOIN runtime_topic_state t ON t.topic_id = s.topic_id
180
+ WHERE (l.topic_id IS NULL OR l.heartbeat_at < ?)
181
+ AND (t.topic_id IS NULL OR t.maintenance = 0 OR t.heartbeat_at < ?)
182
+ AND (
183
+ (s.status = 'pending' AND s.deliver_at <= ?)
184
+ OR (s.status = 'running' AND (s.claimed_at IS NULL OR s.claimed_at < ?))
185
+ )
186
+ ORDER BY CASE s.status WHEN 'running' THEN 0 ELSE 1 END, s.deliver_at ASC
187
+ LIMIT 1`,
188
+ )
189
+ .get(staleBefore, now - TOPIC_MAINTENANCE_STALE_MS, now, staleBefore);
190
+ if (!row) return null;
191
+
192
+ const result = db
193
+ .query(
194
+ `UPDATE runtime_self_schedules
195
+ SET status = 'running', claimed_by = ?, claimed_at = ?, running_query_id = NULL
196
+ WHERE id = ? AND (
197
+ (status = 'pending' AND deliver_at <= ?)
198
+ OR (status = 'running' AND (claimed_at IS NULL OR claimed_at < ?))
199
+ )`,
200
+ )
201
+ .run(ownerId, now, row.id, now, staleBefore);
202
+ if (Number(result.changes ?? 0) === 0) return null;
203
+ return rowToSchedule({
204
+ ...row,
205
+ status: "running",
206
+ claimed_by: ownerId,
207
+ claimed_at: now,
208
+ running_query_id: null,
209
+ });
210
+ })
211
+ .immediate();
212
+ }
213
+
214
+ export function markSelfScheduleRunning(
215
+ scheduleId: string,
216
+ ownerId: string,
217
+ queryId: string,
218
+ ): boolean {
219
+ const result = db
220
+ .query(
221
+ `UPDATE runtime_self_schedules
222
+ SET running_query_id = ?, claimed_at = ?
223
+ WHERE id = ? AND status = 'running' AND claimed_by = ?`,
224
+ )
225
+ .run(queryId, Date.now(), scheduleId, ownerId);
226
+ return Number(result.changes ?? 0) > 0;
227
+ }
228
+
229
+ export function heartbeatSelfScheduleClaim(
230
+ scheduleId: string,
231
+ ownerId: string,
232
+ now = Date.now(),
233
+ ): boolean {
234
+ const result = db
235
+ .query(
236
+ `UPDATE runtime_self_schedules
237
+ SET claimed_at = ?
238
+ WHERE id = ? AND status = 'running' AND claimed_by = ?`,
239
+ )
240
+ .run(now, scheduleId, ownerId);
241
+ return Number(result.changes ?? 0) > 0;
242
+ }
243
+
244
+ /**
245
+ * Put an interrupted run back into the single pending slot. If its own turn
246
+ * already created a newer pending schedule, the newer explicit schedule wins.
247
+ */
248
+ export function releaseSelfScheduleClaim(
249
+ scheduleId: string,
250
+ ownerId: string,
251
+ ): "released" | "dropped" {
252
+ return db
253
+ .transaction(() => {
254
+ const row = db
255
+ .query<SelfScheduleRow, [string, string]>(
256
+ "SELECT * FROM runtime_self_schedules WHERE id = ? AND status = 'running' AND claimed_by = ?",
257
+ )
258
+ .get(scheduleId, ownerId);
259
+ if (!row) return "dropped" as const;
260
+ const newer = getPendingSelfSchedule(row.topic_id);
261
+ if (newer) {
262
+ db.query("DELETE FROM runtime_self_schedules WHERE id = ?").run(scheduleId);
263
+ return "dropped" as const;
264
+ }
265
+ db.query(
266
+ `UPDATE runtime_self_schedules
267
+ SET status = 'pending', claimed_by = NULL, claimed_at = NULL,
268
+ running_query_id = NULL, updated_at = ?
269
+ WHERE id = ? AND claimed_by = ?`,
270
+ ).run(Date.now(), scheduleId, ownerId);
271
+ return "released" as const;
272
+ })
273
+ .immediate();
274
+ }
275
+
276
+ export function completeSelfSchedule(scheduleId: string, ownerId?: string): boolean {
277
+ const result = ownerId
278
+ ? db
279
+ .query("DELETE FROM runtime_self_schedules WHERE id = ? AND claimed_by = ?")
280
+ .run(scheduleId, ownerId)
281
+ : db.query("DELETE FROM runtime_self_schedules WHERE id = ?").run(scheduleId);
282
+ return Number(result.changes ?? 0) > 0;
283
+ }
284
+
285
+ export function deleteSelfSchedulesForTopic(topicId: string): number {
286
+ const result = db.query("DELETE FROM runtime_self_schedules WHERE topic_id = ?").run(topicId);
287
+ return Number(result.changes ?? 0);
288
+ }
289
+
290
+ /** Test/diagnostic helper covering pending and in-flight rows. */
291
+ export function listSelfSchedulesForTopic(topicId: string): SelfSchedule[] {
292
+ return db
293
+ .query<SelfScheduleRow, [string]>(
294
+ "SELECT * FROM runtime_self_schedules WHERE topic_id = ? ORDER BY deliver_at, created_at",
295
+ )
296
+ .all(topicId)
297
+ .map(rowToSchedule);
298
+ }