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,310 @@
1
+ import { getConfigSnapshot } from "../../config/service";
2
+ import type { RuntimeInputPart } from "../../contracts/runtime";
3
+ import { getSessionById } from "../../db/repository";
4
+ import { createLogger } from "../../logging/logger";
5
+ import type { RunService } from "../../run/service";
6
+ import type { AgentRunEvent } from "../../run/types";
7
+ import { createBoundedQueue, type BoundedQueue } from "../boundedQueue";
8
+
9
+ const RUN_STREAM_MAX_QUEUED_FRAMES = 256;
10
+ const RUN_STREAM_DRAIN_DELAY_MS = 25;
11
+ const logger = createLogger("run-event-stream");
12
+
13
+ function runStreamConfig() {
14
+ return getConfigSnapshot().config.runtime.runStream;
15
+ }
16
+
17
+ function parseAfterSeq(req: Request) {
18
+ const url = new URL(req.url);
19
+ const queryRaw = url.searchParams.get("afterSeq") ?? url.searchParams.get("after");
20
+ const headerRaw = req.headers.get("last-event-id");
21
+ const queryAfter = Number(queryRaw);
22
+ const headerAfter = Number(headerRaw);
23
+ const normalizedQuery = Number.isFinite(queryAfter) ? Math.max(0, Math.floor(queryAfter)) : 0;
24
+ const normalizedHeader = Number.isFinite(headerAfter) ? Math.max(0, Math.floor(headerAfter)) : 0;
25
+ return Math.max(normalizedQuery, normalizedHeader);
26
+ }
27
+
28
+ function toRunEventFrame(event: AgentRunEvent) {
29
+ return `id: ${event.seq}\nevent: run-event\ndata: ${JSON.stringify(event)}\n\n`;
30
+ }
31
+
32
+ function toRunHeartbeatFrame(runId: string) {
33
+ return `event: run-heartbeat\ndata: ${JSON.stringify({ runId, at: new Date().toISOString() })}\n\n`;
34
+ }
35
+
36
+ function normalizeRuntimeInputParts(value: unknown): RuntimeInputPart[] {
37
+ if (!Array.isArray(value)) return [];
38
+ const parts: RuntimeInputPart[] = [];
39
+ for (const raw of value) {
40
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
41
+ const record = raw as Record<string, unknown>;
42
+ if (record.type === "text") {
43
+ const text = typeof record.text === "string" ? record.text : "";
44
+ if (!text.trim()) continue;
45
+ parts.push({
46
+ type: "text",
47
+ text,
48
+ });
49
+ continue;
50
+ }
51
+ if (record.type === "file") {
52
+ const mime = typeof record.mime === "string" ? record.mime.trim() : "";
53
+ const url = typeof record.url === "string" ? record.url.trim() : "";
54
+ const filename = typeof record.filename === "string" ? record.filename.trim() || undefined : undefined;
55
+ if (!mime || !url) continue;
56
+ parts.push({
57
+ type: "file",
58
+ mime,
59
+ url,
60
+ filename,
61
+ });
62
+ }
63
+ }
64
+ return parts;
65
+ }
66
+
67
+ export function createRunRoutes(runService: RunService) {
68
+ return {
69
+ "/api/runs": {
70
+ POST: async (req: Request) => {
71
+ const body = (await req.json()) as {
72
+ sessionId?: string;
73
+ content?: string;
74
+ parts?: RuntimeInputPart[];
75
+ agent?: string;
76
+ metadata?: Record<string, unknown>;
77
+ idempotencyKey?: string;
78
+ };
79
+
80
+ const sessionId = body.sessionId?.trim();
81
+ const content = body.content?.trim();
82
+ const parts = normalizeRuntimeInputParts(body.parts);
83
+ if (!sessionId || (!content && parts.length === 0)) {
84
+ return Response.json({ error: "sessionId and content or parts are required" }, { status: 400 });
85
+ }
86
+ if (!getSessionById(sessionId)) {
87
+ return Response.json({ error: "Unknown session" }, { status: 404 });
88
+ }
89
+
90
+ const metadata =
91
+ body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
92
+ ? body.metadata
93
+ : undefined;
94
+ const agent = typeof body.agent === "string" ? body.agent.trim() || undefined : undefined;
95
+ const idempotencyKey = body.idempotencyKey?.trim() || undefined;
96
+
97
+ try {
98
+ const result = runService.createRun({
99
+ sessionId,
100
+ content: content ?? "",
101
+ parts,
102
+ agent,
103
+ metadata,
104
+ idempotencyKey,
105
+ });
106
+ return Response.json(
107
+ {
108
+ accepted: true,
109
+ deduplicated: result.deduplicated,
110
+ runId: result.run.id,
111
+ run: result.run,
112
+ },
113
+ { status: result.deduplicated ? 200 : 202 },
114
+ );
115
+ } catch (error) {
116
+ const message = error instanceof Error ? error.message : "Failed to queue run";
117
+ if (message.startsWith("Unknown session:")) {
118
+ return Response.json({ error: "Unknown session" }, { status: 404 });
119
+ }
120
+ return Response.json({ error: message }, { status: 400 });
121
+ }
122
+ },
123
+ },
124
+
125
+ "/api/runs/:id": {
126
+ GET: async (req: Request & { params: { id: string } }) => {
127
+ const run = runService.getRunById(req.params.id);
128
+ if (!run) {
129
+ return Response.json({ error: "Unknown run" }, { status: 404 });
130
+ }
131
+ return Response.json({ run });
132
+ },
133
+ },
134
+
135
+ "/api/runs/:id/events": {
136
+ GET: async (req: Request & { params: { id: string } }) => {
137
+ const run = runService.getRunById(req.params.id);
138
+ if (!run) {
139
+ return Response.json({ error: "Unknown run" }, { status: 404 });
140
+ }
141
+
142
+ const url = new URL(req.url);
143
+ const afterRaw = url.searchParams.get("afterSeq") ?? url.searchParams.get("after") ?? "0";
144
+ const limitRaw = url.searchParams.get("limit") ?? "100";
145
+ const afterSeq = Number(afterRaw);
146
+ const limit = Number(limitRaw);
147
+
148
+ const replay = runService.listRunEvents({
149
+ runId: run.id,
150
+ afterSeq: Number.isFinite(afterSeq) ? afterSeq : 0,
151
+ limit: Number.isFinite(limit) ? limit : 100,
152
+ });
153
+
154
+ return Response.json({
155
+ runId: run.id,
156
+ afterSeq: Number.isFinite(afterSeq) ? Math.max(0, Math.floor(afterSeq)) : 0,
157
+ events: replay.events,
158
+ nextAfterSeq: replay.nextAfterSeq,
159
+ hasMore: replay.hasMore,
160
+ });
161
+ },
162
+ },
163
+
164
+ "/api/runs/:id/events/stream": {
165
+ GET: (req: Request & { params: { id: string } }) => {
166
+ const run = runService.getRunById(req.params.id);
167
+ if (!run) {
168
+ return Response.json({ error: "Unknown run" }, { status: 404 });
169
+ }
170
+
171
+ const runId = run.id;
172
+ const initialAfterSeq = parseAfterSeq(req);
173
+
174
+ let streamController: ReadableStreamDefaultController<string> | null = null;
175
+ let unsubscribe: (() => void) | null = null;
176
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
177
+ let outboundQueue: BoundedQueue<string> | null = null;
178
+ let closed = false;
179
+ let cursor = initialAfterSeq;
180
+
181
+ const close = () => {
182
+ if (closed) return;
183
+ closed = true;
184
+ outboundQueue?.close();
185
+ outboundQueue = null;
186
+ if (heartbeatTimer) {
187
+ clearInterval(heartbeatTimer);
188
+ heartbeatTimer = null;
189
+ }
190
+ if (unsubscribe) {
191
+ unsubscribe();
192
+ unsubscribe = null;
193
+ }
194
+ if (streamController) {
195
+ try {
196
+ streamController.close();
197
+ } catch {
198
+ // stream already closed
199
+ }
200
+ streamController = null;
201
+ }
202
+ };
203
+
204
+ const emit = (event: AgentRunEvent) => {
205
+ if (closed || !outboundQueue) return;
206
+ if (event.seq <= cursor) return;
207
+ cursor = event.seq;
208
+ outboundQueue.enqueue(toRunEventFrame(event));
209
+ if (event.type === "run.completed" || event.type === "run.failed") {
210
+ if (outboundQueue.size() === 0) {
211
+ close();
212
+ return;
213
+ }
214
+ setTimeout(() => {
215
+ close();
216
+ }, RUN_STREAM_DRAIN_DELAY_MS * 2);
217
+ }
218
+ };
219
+
220
+ const stream = new ReadableStream<string>({
221
+ async start(controller) {
222
+ streamController = controller;
223
+ outboundQueue = createBoundedQueue<string>({
224
+ maxSize: RUN_STREAM_MAX_QUEUED_FRAMES,
225
+ drainDelayMs: RUN_STREAM_DRAIN_DELAY_MS,
226
+ tryWrite: (value) => {
227
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) {
228
+ return false;
229
+ }
230
+ controller.enqueue(value);
231
+ return true;
232
+ },
233
+ onOverflow: () => {
234
+ logger.warn("Closing run SSE consumer", {
235
+ runId,
236
+ reason: "outbound queue overflow",
237
+ });
238
+ close();
239
+ },
240
+ onWriteError: (error) => {
241
+ logger.warnWithCause("Closing run SSE consumer", error, {
242
+ runId,
243
+ reason: "write failure",
244
+ });
245
+ close();
246
+ },
247
+ });
248
+ unsubscribe = runService.subscribe(event => {
249
+ if (event.runId !== runId) return;
250
+ emit(event);
251
+ });
252
+
253
+ while (!closed) {
254
+ const replay = runService.listRunEvents({
255
+ runId,
256
+ afterSeq: cursor,
257
+ limit: runStreamConfig().replayPageSize,
258
+ });
259
+ if (!replay.events.length) break;
260
+ for (const event of replay.events) {
261
+ emit(event);
262
+ if (closed) return;
263
+ }
264
+ if (!replay.hasMore) break;
265
+ }
266
+
267
+ if (closed) return;
268
+ const latestRun = runService.getRunById(runId);
269
+ if (latestRun?.state === "completed" || latestRun?.state === "failed") {
270
+ const deadlineAt = Date.now() + RUN_STREAM_DRAIN_DELAY_MS * 10;
271
+ while (!closed) {
272
+ const finalReplay = runService.listRunEvents({
273
+ runId,
274
+ afterSeq: cursor,
275
+ limit: runStreamConfig().replayPageSize,
276
+ });
277
+ for (const event of finalReplay.events) {
278
+ emit(event);
279
+ if (closed) return;
280
+ }
281
+ if (finalReplay.events.length > 0 || Date.now() >= deadlineAt) {
282
+ break;
283
+ }
284
+ await new Promise(resolve => setTimeout(resolve, RUN_STREAM_DRAIN_DELAY_MS));
285
+ }
286
+ close();
287
+ return;
288
+ }
289
+
290
+ heartbeatTimer = setInterval(() => {
291
+ if (closed || !outboundQueue) return;
292
+ outboundQueue.enqueue(toRunHeartbeatFrame(runId));
293
+ }, runStreamConfig().heartbeatMs);
294
+ },
295
+ cancel() {
296
+ close();
297
+ },
298
+ });
299
+
300
+ return new Response(stream, {
301
+ headers: {
302
+ "Content-Type": "text/event-stream",
303
+ "Cache-Control": "no-cache, no-transform",
304
+ Connection: "keep-alive",
305
+ },
306
+ });
307
+ },
308
+ },
309
+ };
310
+ }
@@ -0,0 +1,197 @@
1
+ import { z } from "zod";
2
+
3
+ import { getOpencodeAgentStorageInfo } from "../../agents/opencodeConfig";
4
+ import { applyConfigPatch, getConfigSnapshot, replaceConfig } from "../../config/service";
5
+ import type { CronService } from "../../cron/service";
6
+ import {
7
+ buildAgentMockingbirdCompactionPrompt,
8
+ buildAgentMockingbirdCompactionContext,
9
+ buildAgentMockingbirdSystemPrompt,
10
+ } from "../../opencode/systemPrompt";
11
+ import { resolveRuntimeSessionScope } from "../../runtime/sessionScope";
12
+
13
+ const runtimePatchSchema = z
14
+ .object({
15
+ workspace: z
16
+ .object({
17
+ pinnedDirectory: z.string().min(1).optional(),
18
+ })
19
+ .partial()
20
+ .optional(),
21
+ runtime: z
22
+ .object({
23
+ memory: z.record(z.string(), z.unknown()).optional(),
24
+ heartbeat: z.record(z.string(), z.unknown()).optional(),
25
+ cron: z.record(z.string(), z.unknown()).optional(),
26
+ })
27
+ .partial()
28
+ .optional(),
29
+ })
30
+ .strict();
31
+
32
+ function buildRuntimePayload() {
33
+ const snapshot = getConfigSnapshot();
34
+ return {
35
+ hash: snapshot.hash,
36
+ path: snapshot.path,
37
+ config: {
38
+ workspace: snapshot.config.workspace,
39
+ runtime: {
40
+ memory: snapshot.config.runtime.memory,
41
+ heartbeat: snapshot.config.runtime.heartbeat,
42
+ cron: snapshot.config.runtime.cron,
43
+ },
44
+ },
45
+ };
46
+ }
47
+
48
+ export function createRuntimeRoutes(input: { cronService: CronService }) {
49
+ return {
50
+ "/api/mockingbird/runtime/pinned-workspace": {
51
+ GET: () => {
52
+ const snapshot = getConfigSnapshot();
53
+ return Response.json({
54
+ directory: snapshot.config.workspace.pinnedDirectory,
55
+ hash: snapshot.hash,
56
+ });
57
+ },
58
+ },
59
+
60
+ "/api/mockingbird/runtime/config": {
61
+ GET: () => Response.json(buildRuntimePayload()),
62
+ PATCH: async (req: Request) => {
63
+ const body = (await req.json()) as Record<string, unknown>;
64
+ const parsed = runtimePatchSchema.safeParse(body.patch ?? body);
65
+ if (!parsed.success) {
66
+ return Response.json(
67
+ { error: parsed.error.issues[0]?.message ?? "Invalid runtime config patch" },
68
+ { status: 400 },
69
+ );
70
+ }
71
+
72
+ try {
73
+ const result = await applyConfigPatch({
74
+ patch: parsed.data,
75
+ expectedHash: typeof body.expectedHash === "string" ? body.expectedHash : undefined,
76
+ runSmokeTest: false,
77
+ });
78
+ return Response.json({
79
+ ...buildRuntimePayload(),
80
+ hash: result.snapshot.hash,
81
+ });
82
+ } catch (error) {
83
+ const message = error instanceof Error ? error.message : "Failed to update runtime config";
84
+ const status = message.includes("refresh and retry") ? 409 : 400;
85
+ return Response.json({ error: message }, { status });
86
+ }
87
+ },
88
+ },
89
+
90
+ "/api/mockingbird/runtime/config/replace": {
91
+ POST: async (req: Request) => {
92
+ const body = (await req.json()) as { config?: unknown; expectedHash?: unknown };
93
+ try {
94
+ const result = await replaceConfig({
95
+ config: body.config,
96
+ expectedHash: typeof body.expectedHash === "string" ? body.expectedHash : undefined,
97
+ runSmokeTest: false,
98
+ });
99
+ return Response.json({
100
+ ...buildRuntimePayload(),
101
+ hash: result.snapshot.hash,
102
+ });
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : "Failed to replace runtime config";
105
+ const status = message.includes("refresh and retry") ? 409 : 400;
106
+ return Response.json({ error: message }, { status });
107
+ }
108
+ },
109
+ },
110
+
111
+ "/api/mockingbird/runtime/info": {
112
+ GET: () => {
113
+ const snapshot = getConfigSnapshot();
114
+ const storage = getOpencodeAgentStorageInfo(snapshot.config);
115
+ return Response.json({
116
+ hash: snapshot.hash,
117
+ path: snapshot.path,
118
+ pinnedWorkspace: snapshot.config.workspace.pinnedDirectory,
119
+ opencode: {
120
+ baseUrl: snapshot.config.runtime.opencode.baseUrl,
121
+ workspaceDirectory: snapshot.config.runtime.opencode.directory,
122
+ configDirectory: storage.configDirectory,
123
+ effectiveConfigPath: storage.configFilePath,
124
+ timeoutMs: snapshot.config.runtime.opencode.timeoutMs,
125
+ },
126
+ });
127
+ },
128
+ },
129
+
130
+ "/api/mockingbird/runtime/system-prompt": {
131
+ GET: () =>
132
+ Response.json({
133
+ system: buildAgentMockingbirdSystemPrompt() ?? "",
134
+ }),
135
+ },
136
+
137
+ "/api/mockingbird/runtime/compaction-context": {
138
+ GET: (req: Request) => {
139
+ const sessionId = new URL(req.url).searchParams.get("sessionId")?.trim() || undefined;
140
+ return Response.json({
141
+ prompt: buildAgentMockingbirdCompactionPrompt(sessionId),
142
+ context: buildAgentMockingbirdCompactionContext(sessionId),
143
+ });
144
+ },
145
+ },
146
+
147
+ "/api/mockingbird/runtime/session-scope": {
148
+ GET: (req: Request) => {
149
+ const sessionId = new URL(req.url).searchParams.get("sessionId")?.trim() ?? "";
150
+ if (!sessionId) {
151
+ return Response.json({ error: "sessionId is required" }, { status: 400 });
152
+ }
153
+
154
+ return Response.json(resolveRuntimeSessionScope(sessionId, input.cronService));
155
+ },
156
+ },
157
+
158
+ "/api/mockingbird/runtime/notify-main-thread": {
159
+ POST: async (req: Request) => {
160
+ const body = (await req.json()) as {
161
+ sessionId?: unknown;
162
+ prompt?: unknown;
163
+ severity?: unknown;
164
+ };
165
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
166
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
167
+ const severity =
168
+ body.severity === "info" || body.severity === "warn" || body.severity === "critical"
169
+ ? body.severity
170
+ : undefined;
171
+ if (!sessionId) {
172
+ return Response.json({ error: "sessionId is required" }, { status: 400 });
173
+ }
174
+ if (!prompt) {
175
+ return Response.json({ error: "prompt is required" }, { status: 400 });
176
+ }
177
+
178
+ try {
179
+ const result = await input.cronService.notifyMainThread({
180
+ runtimeSessionId: sessionId,
181
+ prompt,
182
+ severity,
183
+ });
184
+ return Response.json({ ok: true, ...result });
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : "Failed to notify main thread";
187
+ const status =
188
+ message === "Unknown runtime session" ||
189
+ message === "notify_main_thread is only available from cron or heartbeat threads"
190
+ ? 403
191
+ : 400;
192
+ return Response.json({ error: message }, { status });
193
+ }
194
+ },
195
+ },
196
+ };
197
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ importManagedSkillWithConfigUpdate,
3
+ loadRuntimeSkillCatalog,
4
+ setEnabledSkillsFromCatalog,
5
+ } from "../../config/orchestration";
6
+ import { getConfigSnapshot } from "../../config/service";
7
+ import {
8
+ disposeOpencodeSkillInstance,
9
+ getDisabledSkillsRootPath,
10
+ getManagedSkillsRootPath,
11
+ listManagedSkillCatalog,
12
+ removeManagedSkill,
13
+ setManagedSkillEnabled,
14
+ } from "../../skills/service";
15
+
16
+ export function createSkillRoutes() {
17
+ return {
18
+ "/api/mockingbird/skills": {
19
+ GET: async () => {
20
+ const result = await loadRuntimeSkillCatalog();
21
+ return Response.json(result.payload, { status: result.status });
22
+ },
23
+ },
24
+
25
+ "/api/mockingbird/skills/import": {
26
+ POST: async (req: Request) => {
27
+ const body = (await req.json()) as {
28
+ id?: string;
29
+ content?: string;
30
+ enable?: boolean;
31
+ expectedHash?: string;
32
+ };
33
+ if (!body.id?.trim() || !body.content?.trim()) {
34
+ return Response.json({ error: "id and content are required" }, { status: 400 });
35
+ }
36
+ try {
37
+ const result = await importManagedSkillWithConfigUpdate({
38
+ rawId: body.id,
39
+ content: body.content,
40
+ enable: body.enable !== false,
41
+ expectedHash: body.expectedHash,
42
+ });
43
+ return Response.json(result, { status: 201 });
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : "Failed to import skill";
46
+ const status = message.includes("refresh and retry") ? 409 : 400;
47
+ return Response.json({ error: message }, { status });
48
+ }
49
+ },
50
+ },
51
+
52
+ "/api/mockingbird/skills/enabled": {
53
+ PUT: async (req: Request) => {
54
+ const body = (await req.json()) as { skills?: unknown; expectedHash?: string };
55
+ if (!Array.isArray(body.skills)) {
56
+ return Response.json({ error: "skills must be an array" }, { status: 400 });
57
+ }
58
+ try {
59
+ const result = await setEnabledSkillsFromCatalog({
60
+ skills: body.skills.filter((value): value is string => typeof value === "string"),
61
+ expectedHash: body.expectedHash,
62
+ });
63
+ return Response.json(result);
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : "Failed to update skills";
66
+ const status = message.includes("refresh and retry") ? 409 : 400;
67
+ return Response.json({ error: message }, { status });
68
+ }
69
+ },
70
+ },
71
+
72
+ "/api/mockingbird/skills/:id": {
73
+ PATCH: async (req: Request & { params: { id: string } }) => {
74
+ const body = (await req.json()) as { enabled?: unknown };
75
+ if (typeof body.enabled !== "boolean") {
76
+ return Response.json({ error: "enabled must be a boolean" }, { status: 400 });
77
+ }
78
+ const snapshot = getConfigSnapshot();
79
+ setManagedSkillEnabled(req.params.id, body.enabled, snapshot.config.workspace.pinnedDirectory);
80
+ await disposeOpencodeSkillInstance(snapshot.config);
81
+ const catalog = listManagedSkillCatalog(snapshot.config.workspace.pinnedDirectory);
82
+ return Response.json({
83
+ skills: catalog.skills,
84
+ enabled: catalog.enabled,
85
+ disabled: catalog.disabled,
86
+ invalid: catalog.invalid,
87
+ hash: catalog.revision,
88
+ revision: catalog.revision,
89
+ managedPath: getManagedSkillsRootPath(snapshot.config.workspace.pinnedDirectory),
90
+ disabledPath: getDisabledSkillsRootPath(snapshot.config.workspace.pinnedDirectory),
91
+ });
92
+ },
93
+ DELETE: async (req: Request & { params: { id: string } }) => {
94
+ const snapshot = getConfigSnapshot();
95
+ removeManagedSkill(req.params.id, snapshot.config.workspace.pinnedDirectory);
96
+ await disposeOpencodeSkillInstance(snapshot.config);
97
+ const catalog = listManagedSkillCatalog(snapshot.config.workspace.pinnedDirectory);
98
+ return Response.json({
99
+ removed: true,
100
+ skills: catalog.skills,
101
+ enabled: catalog.enabled,
102
+ disabled: catalog.disabled,
103
+ invalid: catalog.invalid,
104
+ hash: catalog.revision,
105
+ revision: catalog.revision,
106
+ managedPath: getManagedSkillsRootPath(snapshot.config.workspace.pinnedDirectory),
107
+ disabledPath: getDisabledSkillsRootPath(snapshot.config.workspace.pinnedDirectory),
108
+ });
109
+ },
110
+ },
111
+ };
112
+ }