agent-mockingbird 0.0.1

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 (227) hide show
  1. package/.agents/skills/btca-cli/SKILL.md +64 -0
  2. package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
  5. package/.env.example +36 -0
  6. package/.githooks/pre-commit +33 -0
  7. package/.github/workflows/ci.yml +309 -0
  8. package/.opencode/bun.lock +18 -0
  9. package/.opencode/package.json +5 -0
  10. package/.opencode/tools/agent_type_manager.ts +100 -0
  11. package/.opencode/tools/config_manager.ts +87 -0
  12. package/.opencode/tools/cron_manager.ts +145 -0
  13. package/.opencode/tools/memory_get.ts +43 -0
  14. package/.opencode/tools/memory_remember.ts +53 -0
  15. package/.opencode/tools/memory_search.ts +48 -0
  16. package/AGENTS.md +126 -0
  17. package/MEMORY.md +2 -0
  18. package/README.md +451 -0
  19. package/THIRD_PARTY_NOTICES.md +11 -0
  20. package/agent-mockingbird.config.example.json +135 -0
  21. package/apps/server/package.json +32 -0
  22. package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
  23. package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
  24. package/apps/server/src/backend/agents/openclawImport.ts +797 -0
  25. package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
  26. package/apps/server/src/backend/agents/service.ts +10 -0
  27. package/apps/server/src/backend/config/example-config.test.ts +20 -0
  28. package/apps/server/src/backend/config/orchestration.ts +243 -0
  29. package/apps/server/src/backend/config/policy.ts +158 -0
  30. package/apps/server/src/backend/config/schema.test.ts +15 -0
  31. package/apps/server/src/backend/config/schema.ts +391 -0
  32. package/apps/server/src/backend/config/semantic.test.ts +34 -0
  33. package/apps/server/src/backend/config/semantic.ts +149 -0
  34. package/apps/server/src/backend/config/service.test.ts +75 -0
  35. package/apps/server/src/backend/config/service.ts +207 -0
  36. package/apps/server/src/backend/config/smoke.ts +77 -0
  37. package/apps/server/src/backend/config/store.test.ts +123 -0
  38. package/apps/server/src/backend/config/store.ts +581 -0
  39. package/apps/server/src/backend/config/testFixtures.ts +5 -0
  40. package/apps/server/src/backend/config/types.ts +56 -0
  41. package/apps/server/src/backend/contracts/events.ts +320 -0
  42. package/apps/server/src/backend/contracts/runtime.ts +111 -0
  43. package/apps/server/src/backend/cron/executor.ts +435 -0
  44. package/apps/server/src/backend/cron/repository.ts +170 -0
  45. package/apps/server/src/backend/cron/service.ts +660 -0
  46. package/apps/server/src/backend/cron/storage.ts +92 -0
  47. package/apps/server/src/backend/cron/types.ts +138 -0
  48. package/apps/server/src/backend/cron/utils.ts +351 -0
  49. package/apps/server/src/backend/db/client.ts +20 -0
  50. package/apps/server/src/backend/db/migrate.ts +40 -0
  51. package/apps/server/src/backend/db/repository.ts +1762 -0
  52. package/apps/server/src/backend/db/schema.ts +113 -0
  53. package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
  54. package/apps/server/src/backend/db/wipe.ts +13 -0
  55. package/apps/server/src/backend/defaults.ts +32 -0
  56. package/apps/server/src/backend/env.ts +48 -0
  57. package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
  58. package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
  59. package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
  60. package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
  61. package/apps/server/src/backend/heartbeat/service.ts +176 -0
  62. package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
  63. package/apps/server/src/backend/heartbeat/state.ts +167 -0
  64. package/apps/server/src/backend/heartbeat/types.ts +54 -0
  65. package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
  66. package/apps/server/src/backend/http/boundedQueue.ts +92 -0
  67. package/apps/server/src/backend/http/parsers.ts +40 -0
  68. package/apps/server/src/backend/http/router.ts +61 -0
  69. package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
  70. package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
  71. package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
  72. package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
  73. package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
  74. package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
  75. package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
  76. package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
  77. package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
  78. package/apps/server/src/backend/http/routes/index.ts +101 -0
  79. package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
  80. package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
  81. package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
  82. package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
  83. package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
  84. package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
  85. package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
  86. package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
  87. package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
  88. package/apps/server/src/backend/http/schemas.ts +64 -0
  89. package/apps/server/src/backend/http/sse.ts +144 -0
  90. package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
  91. package/apps/server/src/backend/logging/logger.ts +64 -0
  92. package/apps/server/src/backend/mcp/service.ts +326 -0
  93. package/apps/server/src/backend/memory/cli.ts +170 -0
  94. package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
  95. package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
  96. package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
  97. package/apps/server/src/backend/memory/qmdPort.ts +61 -0
  98. package/apps/server/src/backend/memory/records.test.ts +66 -0
  99. package/apps/server/src/backend/memory/records.ts +229 -0
  100. package/apps/server/src/backend/memory/service.ts +2012 -0
  101. package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
  102. package/apps/server/src/backend/memory/types.ts +104 -0
  103. package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
  104. package/apps/server/src/backend/opencode/client.ts +98 -0
  105. package/apps/server/src/backend/opencode/models.ts +41 -0
  106. package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
  107. package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
  108. package/apps/server/src/backend/paths.ts +57 -0
  109. package/apps/server/src/backend/prompts/service.ts +100 -0
  110. package/apps/server/src/backend/queue/queue.test.ts +189 -0
  111. package/apps/server/src/backend/queue/service.ts +177 -0
  112. package/apps/server/src/backend/queue/types.ts +39 -0
  113. package/apps/server/src/backend/run/service.ts +576 -0
  114. package/apps/server/src/backend/run/storage.ts +47 -0
  115. package/apps/server/src/backend/run/types.ts +44 -0
  116. package/apps/server/src/backend/runtime/errors.ts +61 -0
  117. package/apps/server/src/backend/runtime/index.ts +72 -0
  118. package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
  119. package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
  120. package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
  121. package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
  122. package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
  123. package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
  124. package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
  125. package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
  126. package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
  127. package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
  128. package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
  129. package/apps/server/src/backend/skills/service.ts +442 -0
  130. package/apps/server/src/backend/workspace/resolve.ts +27 -0
  131. package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
  132. package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
  133. package/apps/server/src/cli/runtime-assets.mjs +269 -0
  134. package/apps/server/src/cli/runtime-assets.test.ts +52 -0
  135. package/apps/server/src/cli/runtime-layout.mjs +75 -0
  136. package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
  137. package/apps/server/src/cli/standaloneBuild.ts +19 -0
  138. package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
  139. package/apps/server/src/index.ts +178 -0
  140. package/apps/server/tsconfig.json +12 -0
  141. package/backlog.md +5 -0
  142. package/bin/agent-mockingbird +2522 -0
  143. package/bin/runtime-layout.mjs +75 -0
  144. package/build-bin.ts +34 -0
  145. package/build-cli.mjs +37 -0
  146. package/build.ts +40 -0
  147. package/bun-env.d.ts +11 -0
  148. package/bun.lock +888 -0
  149. package/bunfig.toml +2 -0
  150. package/components.json +21 -0
  151. package/config.json +130 -0
  152. package/deploy/RELEASE_INSTALL.md +112 -0
  153. package/deploy/docker-compose.yml +42 -0
  154. package/deploy/systemd/README.md +46 -0
  155. package/deploy/systemd/agent-mockingbird.service +28 -0
  156. package/deploy/systemd/opencode.service +25 -0
  157. package/docs/legacy-config-ui-reference.md +51 -0
  158. package/docs/memory-e2e-trace-2026-03-04.md +63 -0
  159. package/docs/memory-ops.md +96 -0
  160. package/docs/memory-runtime-contract.md +42 -0
  161. package/docs/memory-tuning-remote-2026-03-04.md +59 -0
  162. package/docs/opencode-rebase-workflow-plan.md +614 -0
  163. package/docs/opencode-startup-sync-plan.md +94 -0
  164. package/docs/vendor-opencode.md +41 -0
  165. package/drizzle/0000_famous_turbo.sql +49 -0
  166. package/drizzle/0001_cron_memory_aux.sql +160 -0
  167. package/drizzle/0002_runtime_session_bindings.sql +28 -0
  168. package/drizzle/0003_background_runs.sql +27 -0
  169. package/drizzle/0004_memory_open_write.sql +63 -0
  170. package/drizzle/0005_signal_channel.sql +47 -0
  171. package/drizzle/0006_usage_event_dimensions.sql +7 -0
  172. package/drizzle/meta/0000_snapshot.json +341 -0
  173. package/drizzle/meta/_journal.json +55 -0
  174. package/drizzle.config.ts +14 -0
  175. package/eslint.config.mjs +77 -0
  176. package/knip.json +18 -0
  177. package/memory/2026-03-04.md +4 -0
  178. package/opencode.lock.json +16 -0
  179. package/package.json +67 -0
  180. package/packages/agent-mockingbird-installer/README.md +31 -0
  181. package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
  182. package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
  183. package/packages/agent-mockingbird-installer/package.json +23 -0
  184. package/packages/contracts/package.json +19 -0
  185. package/packages/contracts/src/agentTypes.ts +122 -0
  186. package/packages/contracts/src/cron.ts +146 -0
  187. package/packages/contracts/src/dashboard.ts +378 -0
  188. package/packages/contracts/src/index.ts +3 -0
  189. package/packages/contracts/tsconfig.json +4 -0
  190. package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
  191. package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
  192. package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
  193. package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
  194. package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
  195. package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
  196. package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
  197. package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
  198. package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
  199. package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
  200. package/runtime-assets/opencode-config/opencode.jsonc +25 -0
  201. package/runtime-assets/opencode-config/package.json +5 -0
  202. package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
  203. package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
  204. package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
  205. package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
  206. package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
  207. package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
  208. package/runtime-assets/workspace/AGENTS.md +56 -0
  209. package/runtime-assets/workspace/MEMORY.md +4 -0
  210. package/scripts/build-release-bundle.sh +66 -0
  211. package/scripts/check-ship.ts +383 -0
  212. package/scripts/dev-opencode.sh +17 -0
  213. package/scripts/dev-stack-opencode.sh +15 -0
  214. package/scripts/dev-stack.sh +61 -0
  215. package/scripts/install-systemd.sh +87 -0
  216. package/scripts/memory-e2e.sh +76 -0
  217. package/scripts/memory-trace-e2e.sh +141 -0
  218. package/scripts/migrate-opencode-env.ts +108 -0
  219. package/scripts/onboard/bootstrap.sh +32 -0
  220. package/scripts/opencode-swap.ts +78 -0
  221. package/scripts/opencode-sync.ts +715 -0
  222. package/scripts/runtime-assets-sync.mjs +83 -0
  223. package/scripts/setup-git-hooks.ts +39 -0
  224. package/tsconfig.json +45 -0
  225. package/tui.json +98 -0
  226. package/turbo.json +36 -0
  227. package/vendor/OPENCODE_VENDOR.md +13 -0
@@ -0,0 +1,576 @@
1
+ import { ensureRunTables } from "./storage";
2
+ import type { AgentRun, AgentRunEvent, AgentRunEventType, CreateAgentRunInput } from "./types";
3
+ import type { RuntimeInputPart , RuntimeEngine } from "../contracts/runtime";
4
+ import { sqlite } from "../db/client";
5
+ import { getSessionById } from "../db/repository";
6
+ import { RuntimeContinuationDetachedError, RuntimeSessionQueuedError } from "../runtime/errors";
7
+
8
+ interface AgentRunRow {
9
+ id: string;
10
+ session_id: string;
11
+ state: AgentRun["state"];
12
+ content: string;
13
+ metadata_json: string;
14
+ idempotency_key: string | null;
15
+ result_json: string | null;
16
+ error_json: string | null;
17
+ created_at: number;
18
+ updated_at: number;
19
+ started_at: number | null;
20
+ completed_at: number | null;
21
+ }
22
+
23
+ interface AgentRunEventRow {
24
+ id: string;
25
+ run_id: string;
26
+ seq: number;
27
+ type: AgentRunEventType;
28
+ payload_json: string;
29
+ created_at: number;
30
+ }
31
+
32
+ const nowMs = () => Date.now();
33
+ const toIso = (ms: number) => new Date(ms).toISOString();
34
+ const RUN_ID_PREFIX = "run";
35
+ const RUN_PARTS_METADATA_KEY = "__inputParts";
36
+ type RunEventListener = (event: AgentRunEvent) => void;
37
+
38
+ function parseJson(value: string | null): unknown {
39
+ if (!value) return null;
40
+ try {
41
+ return JSON.parse(value) as unknown;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function normalizeMetadata(value: unknown): Record<string, unknown> {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
49
+ return value as Record<string, unknown>;
50
+ }
51
+
52
+ function normalizeRuntimeInputParts(value: unknown): RuntimeInputPart[] {
53
+ if (!Array.isArray(value)) return [];
54
+ const parts: RuntimeInputPart[] = [];
55
+ for (const raw of value) {
56
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
57
+ const record = raw as Record<string, unknown>;
58
+ if (record.type === "text") {
59
+ const text = typeof record.text === "string" ? record.text : "";
60
+ if (!text.trim()) continue;
61
+ parts.push({
62
+ type: "text",
63
+ text,
64
+ });
65
+ continue;
66
+ }
67
+ if (record.type === "file") {
68
+ const mime = typeof record.mime === "string" ? record.mime.trim() : "";
69
+ const url = typeof record.url === "string" ? record.url.trim() : "";
70
+ const filename = typeof record.filename === "string" ? record.filename.trim() || undefined : undefined;
71
+ if (!mime || !url) continue;
72
+ parts.push({
73
+ type: "file",
74
+ mime,
75
+ filename,
76
+ url,
77
+ });
78
+ }
79
+ }
80
+ return parts;
81
+ }
82
+
83
+ function rowToRun(row: AgentRunRow): AgentRun {
84
+ const metadata = normalizeMetadata(parseJson(row.metadata_json));
85
+ const parts = normalizeRuntimeInputParts(metadata[RUN_PARTS_METADATA_KEY]);
86
+ delete metadata[RUN_PARTS_METADATA_KEY];
87
+ return {
88
+ id: row.id,
89
+ sessionId: row.session_id,
90
+ state: row.state,
91
+ content: row.content,
92
+ parts: parts.length > 0 ? parts : undefined,
93
+ metadata,
94
+ idempotencyKey: row.idempotency_key,
95
+ result: parseJson(row.result_json),
96
+ error: parseJson(row.error_json),
97
+ createdAt: toIso(row.created_at),
98
+ updatedAt: toIso(row.updated_at),
99
+ startedAt: row.started_at ? toIso(row.started_at) : null,
100
+ completedAt: row.completed_at ? toIso(row.completed_at) : null,
101
+ };
102
+ }
103
+
104
+ function rowToRunEvent(row: AgentRunEventRow): AgentRunEvent {
105
+ return {
106
+ id: row.id,
107
+ runId: row.run_id,
108
+ seq: row.seq,
109
+ type: row.type,
110
+ payload: parseJson(row.payload_json),
111
+ at: toIso(row.created_at),
112
+ };
113
+ }
114
+
115
+ function createRunId() {
116
+ return `${RUN_ID_PREFIX}-${crypto.randomUUID().slice(0, 12)}`;
117
+ }
118
+
119
+ function safeJson(value: unknown) {
120
+ return JSON.stringify(value ?? {});
121
+ }
122
+
123
+ function nextRunEventSeq(runId: string) {
124
+ const row = sqlite
125
+ .query(
126
+ `
127
+ SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq
128
+ FROM agent_run_events
129
+ WHERE run_id = ?1
130
+ `,
131
+ )
132
+ .get(runId) as { next_seq: number };
133
+ return row.next_seq;
134
+ }
135
+
136
+ export class RunService {
137
+ private dispatchInFlight = false;
138
+ private recoveredPendingRuns = false;
139
+ private listeners = new Set<RunEventListener>();
140
+ private activeRunIds = new Set<string>();
141
+ private activeSessionIds = new Set<string>();
142
+ private readonly maxConcurrentRuns = 8;
143
+
144
+ constructor(private runtime: RuntimeEngine) {
145
+ ensureRunTables();
146
+ }
147
+
148
+ start() {
149
+ this.ensureRecoveredState();
150
+ void this.kick();
151
+ }
152
+
153
+ stop() {
154
+ // no-op for now; run worker is tick-driven and has no timers
155
+ }
156
+
157
+ subscribe(onEvent: RunEventListener): () => void {
158
+ this.listeners.add(onEvent);
159
+ return () => {
160
+ this.listeners.delete(onEvent);
161
+ };
162
+ }
163
+
164
+ createRun(input: CreateAgentRunInput): { run: AgentRun; deduplicated: boolean } {
165
+ ensureRunTables();
166
+ this.ensureRecoveredState();
167
+
168
+ const sessionId = input.sessionId.trim();
169
+ const content = input.content.trim();
170
+ const parts = normalizeRuntimeInputParts(input.parts);
171
+ const session = getSessionById(sessionId);
172
+ if (!session) {
173
+ throw new Error(`Unknown session: ${sessionId}`);
174
+ }
175
+ if (!content && parts.length === 0) {
176
+ throw new Error("content or parts is required");
177
+ }
178
+
179
+ const idempotencyKey = input.idempotencyKey?.trim() || null;
180
+ if (idempotencyKey) {
181
+ const existing = sqlite
182
+ .query(
183
+ `
184
+ SELECT *
185
+ FROM agent_runs
186
+ WHERE idempotency_key = ?1
187
+ LIMIT 1
188
+ `,
189
+ )
190
+ .get(idempotencyKey) as AgentRunRow | null;
191
+ if (existing) {
192
+ return {
193
+ run: rowToRun(existing),
194
+ deduplicated: true,
195
+ };
196
+ }
197
+ }
198
+
199
+ const runId = createRunId();
200
+ const createdAt = nowMs();
201
+ const metadata = normalizeMetadata(input.metadata);
202
+ if (parts.length > 0) {
203
+ metadata[RUN_PARTS_METADATA_KEY] = parts;
204
+ }
205
+ const agent = input.agent?.trim();
206
+ if (agent) {
207
+ metadata.agent = agent;
208
+ }
209
+ const tx = sqlite.transaction(() => {
210
+ sqlite
211
+ .query(
212
+ `
213
+ INSERT INTO agent_runs (
214
+ id, session_id, state, content, metadata_json, idempotency_key,
215
+ result_json, error_json, created_at, updated_at, started_at, completed_at
216
+ )
217
+ VALUES (?1, ?2, 'queued', ?3, ?4, ?5, NULL, NULL, ?6, ?6, NULL, NULL)
218
+ `,
219
+ )
220
+ .run(runId, sessionId, content, safeJson(metadata), idempotencyKey, createdAt);
221
+
222
+ this.insertRunEvent(
223
+ runId,
224
+ "run.accepted",
225
+ { sessionId, idempotencyKey, agent: agent ?? null, hasParts: parts.length > 0 },
226
+ createdAt,
227
+ );
228
+ });
229
+ tx();
230
+
231
+ const run = this.getRunById(runId);
232
+ if (!run) {
233
+ throw new Error(`Failed to load created run: ${runId}`);
234
+ }
235
+
236
+ void this.kick();
237
+ return { run, deduplicated: false };
238
+ }
239
+
240
+ getRunById(runId: string): AgentRun | null {
241
+ ensureRunTables();
242
+ const row = sqlite
243
+ .query(
244
+ `
245
+ SELECT *
246
+ FROM agent_runs
247
+ WHERE id = ?1
248
+ `,
249
+ )
250
+ .get(runId) as AgentRunRow | null;
251
+ return row ? rowToRun(row) : null;
252
+ }
253
+
254
+ listRunEvents(input: {
255
+ runId: string;
256
+ afterSeq?: number;
257
+ limit?: number;
258
+ }): { events: AgentRunEvent[]; hasMore: boolean; nextAfterSeq: number } {
259
+ ensureRunTables();
260
+ const afterSeq = Math.max(0, Math.floor(input.afterSeq ?? 0));
261
+ const limit = Math.max(1, Math.min(500, Math.floor(input.limit ?? 100)));
262
+ const rows = sqlite
263
+ .query(
264
+ `
265
+ SELECT *
266
+ FROM agent_run_events
267
+ WHERE run_id = ?1
268
+ AND seq > ?2
269
+ ORDER BY seq ASC
270
+ LIMIT ?3
271
+ `,
272
+ )
273
+ .all(input.runId, afterSeq, limit + 1) as AgentRunEventRow[];
274
+ const hasMore = rows.length > limit;
275
+ const trimmed = hasMore ? rows.slice(0, limit) : rows;
276
+ const events = trimmed.map(rowToRunEvent);
277
+ const nextAfterSeq = events.length ? events[events.length - 1]!.seq : afterSeq;
278
+ return { events, hasMore, nextAfterSeq };
279
+ }
280
+
281
+ private ensureRecoveredState() {
282
+ if (this.recoveredPendingRuns) return;
283
+ this.recoveredPendingRuns = true;
284
+
285
+ const runningRows = sqlite
286
+ .query(
287
+ `
288
+ SELECT id
289
+ FROM agent_runs
290
+ WHERE state = 'running'
291
+ `,
292
+ )
293
+ .all() as Array<{ id: string }>;
294
+ if (!runningRows.length) return;
295
+
296
+ const recoveredAt = nowMs();
297
+ const tx = sqlite.transaction(() => {
298
+ sqlite
299
+ .query(
300
+ `
301
+ UPDATE agent_runs
302
+ SET state = 'queued', updated_at = ?1, started_at = NULL
303
+ WHERE state = 'running'
304
+ `,
305
+ )
306
+ .run(recoveredAt);
307
+ for (const row of runningRows) {
308
+ this.insertRunEvent(row.id, "run.recovered", { reason: "service restart" }, recoveredAt);
309
+ }
310
+ });
311
+ tx();
312
+ }
313
+
314
+ private async kick() {
315
+ if (this.dispatchInFlight) return;
316
+ this.dispatchInFlight = true;
317
+ try {
318
+ while (true) {
319
+ if (this.activeRunIds.size >= this.maxConcurrentRuns) break;
320
+ const claimed = this.claimNextQueuedRun(this.activeSessionIds);
321
+ if (!claimed) break;
322
+ this.activeRunIds.add(claimed.id);
323
+ this.activeSessionIds.add(claimed.session_id);
324
+ void this.executeRun(claimed)
325
+ .catch(() => {
326
+ // executeRun persists failures; dispatch loop only needs cleanup.
327
+ })
328
+ .finally(() => {
329
+ this.activeRunIds.delete(claimed.id);
330
+ this.activeSessionIds.delete(claimed.session_id);
331
+ void this.kick();
332
+ });
333
+ }
334
+ } finally {
335
+ this.dispatchInFlight = false;
336
+ if (this.hasRunnableQueuedRuns()) {
337
+ void this.kick();
338
+ }
339
+ }
340
+ }
341
+
342
+ private hasRunnableQueuedRuns() {
343
+ if (this.activeRunIds.size >= this.maxConcurrentRuns) return false;
344
+ if (this.activeSessionIds.size === 0) {
345
+ const row = sqlite
346
+ .query(
347
+ `
348
+ SELECT COUNT(*) as count
349
+ FROM agent_runs
350
+ WHERE state = 'queued'
351
+ `,
352
+ )
353
+ .get() as { count: number };
354
+ return row.count > 0;
355
+ }
356
+
357
+ const excludedSessionIds = [...this.activeSessionIds];
358
+ const placeholders = excludedSessionIds.map((_, index) => `?${index + 1}`).join(", ");
359
+ const row = sqlite
360
+ .query(
361
+ `
362
+ SELECT COUNT(*) as count
363
+ FROM agent_runs
364
+ WHERE state = 'queued'
365
+ AND session_id NOT IN (${placeholders})
366
+ `,
367
+ )
368
+ .get(...excludedSessionIds) as { count: number };
369
+ return row.count > 0;
370
+ }
371
+
372
+ private claimNextQueuedRun(excludedSessionIds: Set<string>): AgentRunRow | null {
373
+ ensureRunTables();
374
+ const excluded = [...excludedSessionIds];
375
+ const notInClause =
376
+ excluded.length > 0
377
+ ? `AND session_id NOT IN (${excluded.map((_, index) => `?${index + 1}`).join(", ")})`
378
+ : "";
379
+ const tx = sqlite.transaction(() => {
380
+ const next = sqlite
381
+ .query(
382
+ `
383
+ SELECT *
384
+ FROM agent_runs
385
+ WHERE state = 'queued'
386
+ ${notInClause}
387
+ ORDER BY created_at ASC
388
+ LIMIT 1
389
+ `,
390
+ )
391
+ .get(...excluded) as AgentRunRow | null;
392
+ if (!next) return null;
393
+
394
+ const claimedAt = nowMs();
395
+ sqlite
396
+ .query(
397
+ `
398
+ UPDATE agent_runs
399
+ SET state = 'running', started_at = COALESCE(started_at, ?2), updated_at = ?2
400
+ WHERE id = ?1
401
+ AND state = 'queued'
402
+ `,
403
+ )
404
+ .run(next.id, claimedAt);
405
+ const changed = sqlite.query("SELECT changes() as count").get() as { count: number };
406
+ if (changed.count < 1) return null;
407
+
408
+ const claimed = sqlite
409
+ .query(
410
+ `
411
+ SELECT *
412
+ FROM agent_runs
413
+ WHERE id = ?1
414
+ `,
415
+ )
416
+ .get(next.id) as AgentRunRow | null;
417
+ return claimed;
418
+ });
419
+ return tx();
420
+ }
421
+
422
+ private async executeRun(run: AgentRunRow) {
423
+ const metadata = normalizeMetadata(parseJson(run.metadata_json));
424
+ const parts = normalizeRuntimeInputParts(metadata[RUN_PARTS_METADATA_KEY]);
425
+ delete metadata[RUN_PARTS_METADATA_KEY];
426
+ const agent = typeof metadata.agent === "string" ? metadata.agent.trim() : "";
427
+ const startedAt = nowMs();
428
+ this.insertRunEvent(run.id, "run.started", { sessionId: run.session_id, agent: agent || null }, startedAt);
429
+
430
+ try {
431
+ const ack = await this.runtime.sendUserMessage({
432
+ sessionId: run.session_id,
433
+ content: run.content,
434
+ parts,
435
+ agent: agent || undefined,
436
+ metadata,
437
+ });
438
+
439
+ const completedAt = nowMs();
440
+ const result = {
441
+ sessionId: ack.sessionId,
442
+ messageCount: ack.messages.length,
443
+ messageIds: ack.messages.map(message => message.id),
444
+ };
445
+
446
+ const tx = sqlite.transaction(() => {
447
+ sqlite
448
+ .query(
449
+ `
450
+ UPDATE agent_runs
451
+ SET
452
+ state = 'completed',
453
+ updated_at = ?2,
454
+ completed_at = ?2,
455
+ result_json = ?3,
456
+ error_json = NULL
457
+ WHERE id = ?1
458
+ `,
459
+ )
460
+ .run(run.id, completedAt, safeJson(result));
461
+ this.insertRunEvent(run.id, "run.completed", result, completedAt);
462
+ });
463
+ tx();
464
+ } catch (error) {
465
+ if (error instanceof RuntimeContinuationDetachedError) {
466
+ const completedAt = nowMs();
467
+ const result = {
468
+ sessionId: run.session_id,
469
+ detached: true,
470
+ childRunCount: error.childRunCount,
471
+ };
472
+ const tx = sqlite.transaction(() => {
473
+ sqlite
474
+ .query(
475
+ `
476
+ UPDATE agent_runs
477
+ SET
478
+ state = 'completed',
479
+ updated_at = ?2,
480
+ completed_at = ?2,
481
+ result_json = ?3,
482
+ error_json = NULL
483
+ WHERE id = ?1
484
+ `,
485
+ )
486
+ .run(run.id, completedAt, safeJson(result));
487
+ this.insertRunEvent(run.id, "run.completed", result, completedAt);
488
+ });
489
+ tx();
490
+ return;
491
+ }
492
+
493
+ if (error instanceof RuntimeSessionQueuedError) {
494
+ const completedAt = nowMs();
495
+ const result = {
496
+ sessionId: run.session_id,
497
+ queued: true,
498
+ queueDepth: error.depth,
499
+ };
500
+ const tx = sqlite.transaction(() => {
501
+ sqlite
502
+ .query(
503
+ `
504
+ UPDATE agent_runs
505
+ SET
506
+ state = 'completed',
507
+ updated_at = ?2,
508
+ completed_at = ?2,
509
+ result_json = ?3,
510
+ error_json = NULL
511
+ WHERE id = ?1
512
+ `,
513
+ )
514
+ .run(run.id, completedAt, safeJson(result));
515
+ this.insertRunEvent(run.id, "run.completed", result, completedAt);
516
+ });
517
+ tx();
518
+ return;
519
+ }
520
+
521
+ const failedAt = nowMs();
522
+ const details = {
523
+ name: error instanceof Error ? error.name : "Error",
524
+ message: error instanceof Error ? error.message : "Run execution failed",
525
+ };
526
+ const tx = sqlite.transaction(() => {
527
+ sqlite
528
+ .query(
529
+ `
530
+ UPDATE agent_runs
531
+ SET
532
+ state = 'failed',
533
+ updated_at = ?2,
534
+ completed_at = ?2,
535
+ error_json = ?3
536
+ WHERE id = ?1
537
+ `,
538
+ )
539
+ .run(run.id, failedAt, safeJson(details));
540
+ this.insertRunEvent(run.id, "run.failed", details, failedAt);
541
+ });
542
+ tx();
543
+ }
544
+ }
545
+
546
+ private insertRunEvent(runId: string, type: AgentRunEventType, payload: unknown, createdAt = nowMs()) {
547
+ const seq = nextRunEventSeq(runId);
548
+ const eventId = crypto.randomUUID();
549
+ sqlite
550
+ .query(
551
+ `
552
+ INSERT INTO agent_run_events (
553
+ id, run_id, seq, type, payload_json, created_at
554
+ )
555
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)
556
+ `,
557
+ )
558
+ .run(eventId, runId, seq, type, safeJson(payload), createdAt);
559
+
560
+ const event: AgentRunEvent = {
561
+ id: eventId,
562
+ runId,
563
+ seq,
564
+ type,
565
+ payload,
566
+ at: toIso(createdAt),
567
+ };
568
+ for (const listener of this.listeners) {
569
+ try {
570
+ listener(event);
571
+ } catch {
572
+ // Best-effort only; listener failures should not affect run execution.
573
+ }
574
+ }
575
+ }
576
+ }
@@ -0,0 +1,47 @@
1
+ import { sqlite } from "../db/client";
2
+
3
+ export function ensureRunTables() {
4
+ sqlite.exec(`
5
+ CREATE TABLE IF NOT EXISTS agent_runs (
6
+ id TEXT PRIMARY KEY NOT NULL,
7
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
8
+ state TEXT NOT NULL CHECK (state IN ('queued', 'running', 'completed', 'failed')),
9
+ content TEXT NOT NULL,
10
+ metadata_json TEXT NOT NULL DEFAULT '{}',
11
+ idempotency_key TEXT,
12
+ result_json TEXT,
13
+ error_json TEXT,
14
+ created_at INTEGER NOT NULL,
15
+ updated_at INTEGER NOT NULL,
16
+ started_at INTEGER,
17
+ completed_at INTEGER
18
+ );
19
+
20
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_runs_idempotency_idx
21
+ ON agent_runs(idempotency_key)
22
+ WHERE idempotency_key IS NOT NULL;
23
+ CREATE INDEX IF NOT EXISTS agent_runs_state_created_idx
24
+ ON agent_runs(state, created_at ASC);
25
+ CREATE INDEX IF NOT EXISTS agent_runs_session_created_idx
26
+ ON agent_runs(session_id, created_at DESC);
27
+
28
+ CREATE TABLE IF NOT EXISTS agent_run_events (
29
+ id TEXT PRIMARY KEY NOT NULL,
30
+ run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
31
+ seq INTEGER NOT NULL,
32
+ type TEXT NOT NULL,
33
+ payload_json TEXT NOT NULL,
34
+ created_at INTEGER NOT NULL,
35
+ UNIQUE(run_id, seq)
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS agent_run_events_run_seq_idx
39
+ ON agent_run_events(run_id, seq ASC);
40
+ `);
41
+ }
42
+
43
+ export function clearRunTables() {
44
+ ensureRunTables();
45
+ sqlite.query("DELETE FROM agent_run_events").run();
46
+ sqlite.query("DELETE FROM agent_runs").run();
47
+ }
@@ -0,0 +1,44 @@
1
+ import type { RuntimeInputPart } from "../contracts/runtime";
2
+
3
+ export type AgentRunState = "queued" | "running" | "completed" | "failed";
4
+
5
+ export type AgentRunEventType =
6
+ | "run.accepted"
7
+ | "run.recovered"
8
+ | "run.started"
9
+ | "run.completed"
10
+ | "run.failed";
11
+
12
+ export interface AgentRun {
13
+ id: string;
14
+ sessionId: string;
15
+ state: AgentRunState;
16
+ content: string;
17
+ parts?: RuntimeInputPart[];
18
+ metadata: Record<string, unknown>;
19
+ idempotencyKey: string | null;
20
+ result: unknown;
21
+ error: unknown;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ startedAt: string | null;
25
+ completedAt: string | null;
26
+ }
27
+
28
+ export interface AgentRunEvent {
29
+ id: string;
30
+ runId: string;
31
+ seq: number;
32
+ type: AgentRunEventType;
33
+ payload: unknown;
34
+ at: string;
35
+ }
36
+
37
+ export interface CreateAgentRunInput {
38
+ sessionId: string;
39
+ content: string;
40
+ parts?: RuntimeInputPart[];
41
+ agent?: string;
42
+ metadata?: Record<string, unknown>;
43
+ idempotencyKey?: string;
44
+ }