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,705 @@
1
+ import type { RuntimeEvent } from "../../contracts/events";
2
+ import {
3
+ createHeartbeatUpdatedEvent,
4
+ createSessionMessageCreatedEvent,
5
+ createSessionRunErrorEvent,
6
+ createSessionRunStatusUpdatedEvent,
7
+ createSessionStateUpdatedEvent,
8
+ createUsageUpdatedEvent,
9
+ } from "../../contracts/events";
10
+ import {
11
+ appendChatExchange,
12
+ createBackgroundRun,
13
+ getBackgroundRunById,
14
+ getBackgroundRunByChildExternalSessionId,
15
+ getRuntimeSessionBinding,
16
+ getSessionById,
17
+ setBackgroundRunStatus,
18
+ setMessageMemoryTrace,
19
+ listBackgroundRunsForParentSession,
20
+ listInFlightBackgroundRuns,
21
+ listRecentBackgroundRuns,
22
+ } from "../../db/repository";
23
+ import {
24
+ normalizeMcpIds,
25
+ normalizeMcpServerDefinitions,
26
+ } from "../../mcp/service";
27
+ import {
28
+ createOpencodeClient,
29
+ createOpencodeClientFromConnection,
30
+ getOpencodeErrorStatus,
31
+ unwrapSdkData,
32
+ } from "../../opencode/client";
33
+ import { getLaneQueue } from "../../queue/service";
34
+ import { normalizeSkillIds } from "../../skills/service";
35
+ import {
36
+ RuntimeContinuationDetachedError,
37
+ RuntimeSessionBusyError,
38
+ RuntimeSessionNotFoundError,
39
+ RuntimeSessionQueuedError,
40
+ } from "../errors";
41
+ import type { OpencodeRuntime } from "../opencodeRuntime";
42
+ import {
43
+ isQueueDrainRequest,
44
+ OPENCODE_RUNTIME_ID,
45
+ shouldQueueWhenBusy,
46
+ type BackgroundRunHandle,
47
+ type ListBackgroundRunsInput,
48
+ type OpencodeSessionStatus,
49
+ type PromptBackgroundAsyncInput,
50
+ type ResolvedModel,
51
+ type RuntimeHealthCheckInput,
52
+ type RuntimeHealthCheckResult,
53
+ type RuntimeMessageAck,
54
+ type SendUserMessageInput,
55
+ type SpawnBackgroundSessionInput,
56
+ type AssistantInfo,
57
+ type Part,
58
+ type Session,
59
+ } from "./shared";
60
+
61
+ export interface OpencodeRuntimeCoreMethods {
62
+ syncSessionMessages(sessionId: string): Promise<void>;
63
+ checkHealth(input?: RuntimeHealthCheckInput): Promise<RuntimeHealthCheckResult>;
64
+ currentRuntimeConfig(): ReturnType<NonNullable<OpencodeRuntime["options"]["getRuntimeConfig"]>> | undefined;
65
+ currentProviderId(): string;
66
+ currentModelId(): string;
67
+ currentFallbackModels(): string[];
68
+ currentSmallModel(): string;
69
+ currentTimeoutMs(): number;
70
+ currentPromptTimeoutMs(): number;
71
+ currentEnabledSkills(): string[];
72
+ currentEnabledMcps(): string[];
73
+ currentConfiguredMcpServers(): ReturnType<typeof normalizeMcpServerDefinitions>;
74
+ getClient(): ReturnType<typeof createOpencodeClient>;
75
+ sendUserMessage(input: SendUserMessageInput): Promise<RuntimeMessageAck>;
76
+ spawnBackgroundSession(input: SpawnBackgroundSessionInput): Promise<BackgroundRunHandle>;
77
+ promptBackgroundAsync(input: PromptBackgroundAsyncInput): Promise<BackgroundRunHandle>;
78
+ getBackgroundStatus(runId: string): Promise<BackgroundRunHandle | null>;
79
+ listBackgroundRuns(input?: ListBackgroundRunsInput): Promise<Array<BackgroundRunHandle>>;
80
+ abortBackground(runId: string): Promise<boolean>;
81
+ abortSession(sessionId: string): Promise<boolean>;
82
+ compactSession(sessionId: string): Promise<boolean>;
83
+ modelSupportsImageInput(model: ResolvedModel): Promise<boolean>;
84
+ currentImageModel(): string;
85
+ emit(event: RuntimeEvent): void;
86
+ }
87
+
88
+ export const opencodeRuntimeCoreMethods: OpencodeRuntimeCoreMethods = {
89
+ async syncSessionMessages(this: OpencodeRuntime, sessionId) {
90
+ const localSessionId = sessionId.trim();
91
+ if (!localSessionId) return;
92
+ const localSession = getSessionById(localSessionId);
93
+ if (!localSession) return;
94
+ const externalSessionId = getRuntimeSessionBinding(OPENCODE_RUNTIME_ID, localSessionId);
95
+ if (!externalSessionId) return;
96
+ const run = getBackgroundRunByChildExternalSessionId(OPENCODE_RUNTIME_ID, externalSessionId);
97
+ if (run) {
98
+ this.ensureLocalSessionForBackgroundRun(run);
99
+ }
100
+ await this.syncLocalSessionFromOpencode({
101
+ localSessionId,
102
+ externalSessionId,
103
+ force: true,
104
+ titleHint: localSession.title,
105
+ });
106
+ },
107
+
108
+ async checkHealth(this: OpencodeRuntime, input) {
109
+ const force = input?.force === true;
110
+ if (!force && this["healthSnapshot"] && this["healthCacheExpiresAtMs"] > Date.now()) {
111
+ return { ...this["healthSnapshot"], fromCache: true };
112
+ }
113
+ await this.ensureRuntimeConfigSynced();
114
+ if (!this["healthProbeInFlight"]) {
115
+ this["healthProbeInFlight"] = this.runHealthProbe();
116
+ }
117
+ try {
118
+ const snapshot = await this["healthProbeInFlight"];
119
+ this["healthSnapshot"] = snapshot;
120
+ this["healthCacheExpiresAtMs"] = Date.parse(snapshot.cacheExpiresAt);
121
+ return { ...snapshot, fromCache: false };
122
+ } finally {
123
+ this["healthProbeInFlight"] = null;
124
+ }
125
+ },
126
+
127
+ currentRuntimeConfig(this: OpencodeRuntime) {
128
+ return this["options"].getRuntimeConfig?.();
129
+ },
130
+
131
+ currentProviderId(this: OpencodeRuntime) {
132
+ return this.currentRuntimeConfig()?.providerId?.trim() || this["options"].defaultProviderId;
133
+ },
134
+
135
+ currentModelId(this: OpencodeRuntime) {
136
+ return this.currentRuntimeConfig()?.modelId?.trim() || this["options"].defaultModelId;
137
+ },
138
+
139
+ currentFallbackModels(this: OpencodeRuntime) {
140
+ return this.currentRuntimeConfig()?.fallbackModels ?? this["options"].fallbackModelRefs ?? [];
141
+ },
142
+
143
+ currentSmallModel(this: OpencodeRuntime) {
144
+ const smallModel = this.currentRuntimeConfig()?.smallModel?.trim();
145
+ return smallModel || `${this.currentProviderId()}/${this.currentModelId()}`;
146
+ },
147
+
148
+ currentTimeoutMs(this: OpencodeRuntime) {
149
+ return this.currentRuntimeConfig()?.timeoutMs ?? 120_000;
150
+ },
151
+
152
+ currentPromptTimeoutMs(this: OpencodeRuntime) {
153
+ return this.currentRuntimeConfig()?.promptTimeoutMs ?? 300_000;
154
+ },
155
+
156
+ currentEnabledSkills(this: OpencodeRuntime) {
157
+ return normalizeSkillIds(this["options"].getEnabledSkills?.() ?? []);
158
+ },
159
+
160
+ currentEnabledMcps(this: OpencodeRuntime) {
161
+ return normalizeMcpIds(this["options"].getEnabledMcps?.() ?? []);
162
+ },
163
+
164
+ currentConfiguredMcpServers(this: OpencodeRuntime) {
165
+ return normalizeMcpServerDefinitions(this["options"].getConfiguredMcpServers?.() ?? []);
166
+ },
167
+
168
+ getClient(this: OpencodeRuntime) {
169
+ if (this["options"].client) return this["options"].client;
170
+ const runtimeConfig = this.currentRuntimeConfig();
171
+ if (!runtimeConfig) {
172
+ if (!this["client"]) this["client"] = createOpencodeClient();
173
+ return this["client"];
174
+ }
175
+ const nextKey = `${runtimeConfig.baseUrl}|${runtimeConfig.directory ?? ""}`;
176
+ if (!this["client"] || this["clientConnectionKey"] !== nextKey) {
177
+ this["clientConnectionKey"] = nextKey;
178
+ this["runtimeConfigSyncKey"] = null;
179
+ this["client"] = createOpencodeClientFromConnection({
180
+ baseUrl: runtimeConfig.baseUrl,
181
+ directory: runtimeConfig.directory,
182
+ });
183
+ }
184
+ return this["client"];
185
+ },
186
+
187
+ async sendUserMessage(this: OpencodeRuntime, input) {
188
+ const session = getSessionById(input.sessionId);
189
+ if (!session) throw new RuntimeSessionNotFoundError(input.sessionId);
190
+ const childRunsInFlight = this.inFlightBackgroundChildRunCount(session.id);
191
+ const sessionBusy =
192
+ this["busySessions"].has(session.id) ||
193
+ childRunsInFlight > 0 ||
194
+ (this["drainingSessions"].has(session.id) && !isQueueDrainRequest(input));
195
+ if (sessionBusy) {
196
+ if (shouldQueueWhenBusy(input)) {
197
+ let enqueuedDepth = 0;
198
+ let queued = false;
199
+ try {
200
+ const queue = getLaneQueue();
201
+ const enqueued = queue.enqueue(
202
+ session.id,
203
+ input.content,
204
+ input.parts,
205
+ input.agent,
206
+ input.metadata,
207
+ );
208
+ enqueuedDepth = enqueued.depth;
209
+ queued = enqueued.queued;
210
+ } catch {
211
+ // Queue not initialized, fall through
212
+ }
213
+ if (queued) throw new RuntimeSessionQueuedError(session.id, enqueuedDepth);
214
+ }
215
+ throw new RuntimeSessionBusyError(session.id);
216
+ }
217
+ this["busySessions"].add(session.id);
218
+
219
+ try {
220
+ await this.ensureRuntimeConfigSynced();
221
+ const model = this.resolveModel(session.model);
222
+ let selectedModel = model;
223
+ let opencodeSessionId = await this.resolveOrCreateOpencodeSession(session.id, session.title);
224
+ const inputParts = this.normalizePromptInputParts(input.content, input.parts);
225
+ const imageInputPresent = inputParts.some(
226
+ (part) => part.type === "file" && part.mime.toLowerCase().startsWith("image/"),
227
+ );
228
+ if (imageInputPresent) {
229
+ const supportsImage = await this.modelSupportsImageInput(model);
230
+ if (!supportsImage) {
231
+ const imageModelRef = this.currentImageModel();
232
+ if (imageModelRef) {
233
+ selectedModel = this.resolveModel(imageModelRef);
234
+ this.emit(
235
+ createSessionRunStatusUpdatedEvent(
236
+ {
237
+ sessionId: session.id,
238
+ status: "retry",
239
+ attempt: 1,
240
+ message: `Routing image input to ${this.formatModelRef(selectedModel)} in this session.`,
241
+ },
242
+ "runtime",
243
+ ),
244
+ );
245
+ }
246
+ }
247
+ }
248
+
249
+ const primaryText = this.extractPrimaryTextInput(inputParts);
250
+ const promptInput = await this.buildPromptInputWithMemory(opencodeSessionId, primaryText);
251
+ const requestedAgent = await this.resolveRequestedAgentId(input.agent?.trim(), session.id);
252
+ const effectiveAgent =
253
+ requestedAgent ??
254
+ (await this.resolvePrimaryAgentId(undefined, { emitRetryStatus: false }));
255
+ const promptParts = this.applyMemoryPromptToParts(inputParts, promptInput.content);
256
+ const recreatedSessionPromptParts = this.applyMemoryPromptToParts(
257
+ inputParts,
258
+ promptInput.freshSessionContent,
259
+ );
260
+
261
+ let promptResult: {
262
+ message: { info: AssistantInfo; parts: Array<Part> };
263
+ opencodeSessionId: string;
264
+ };
265
+ try {
266
+ promptResult = await this.sendPromptWithModelFallback({
267
+ localSessionId: session.id,
268
+ localSessionTitle: session.title,
269
+ opencodeSessionId,
270
+ primaryModel: selectedModel,
271
+ parts: promptParts,
272
+ retryPartsOnSessionRecreate: recreatedSessionPromptParts,
273
+ memoryContextFingerprint: promptInput.memoryContextFingerprint,
274
+ system: undefined,
275
+ agent: effectiveAgent,
276
+ });
277
+ } catch (error) {
278
+ const childRunCount = this.inFlightBackgroundChildRunCount(session.id);
279
+ if (this.isTimeoutLikeError(error) && childRunCount > 0) {
280
+ throw new RuntimeContinuationDetachedError(session.id, childRunCount);
281
+ }
282
+ throw error;
283
+ }
284
+
285
+ opencodeSessionId = promptResult.opencodeSessionId;
286
+ const assistantMessage = promptResult.message;
287
+ this.rememberMessageRole(opencodeSessionId, assistantMessage.info.id, "assistant");
288
+ for (const part of assistantMessage.parts) {
289
+ this.rememberPartMetadata(part);
290
+ }
291
+
292
+ await this.syncSessionTitleFromOpencode(session.id, opencodeSessionId, session.title);
293
+ this.startSessionTitlePolling(session.id, opencodeSessionId);
294
+
295
+ const trace = this.buildMessageMemoryTrace(assistantMessage.parts, {
296
+ injectedContextResults: promptInput.injectedContextResults,
297
+ retrievedContextResults: promptInput.retrievedContextResults,
298
+ suppressedAsAlreadyInContext: promptInput.suppressedAsAlreadyInContext,
299
+ suppressedAsIrrelevant: promptInput.suppressedAsIrrelevant,
300
+ });
301
+ const assistantParts = this.buildChatMessageParts(assistantMessage.parts);
302
+ const assistantError = this.extractAssistantError(
303
+ assistantMessage.info,
304
+ assistantMessage.parts,
305
+ );
306
+ if (assistantError) {
307
+ const normalizedAssistantError =
308
+ this.normalizeProviderMessage(assistantError) || assistantError;
309
+ this.emit(
310
+ createSessionRunErrorEvent(
311
+ { sessionId: session.id, message: normalizedAssistantError },
312
+ "runtime",
313
+ ),
314
+ );
315
+ throw new Error(`OpenCode run failed: ${normalizedAssistantError}`);
316
+ }
317
+
318
+ const assistantText =
319
+ this.extractText(assistantMessage.parts) ||
320
+ this.mapOpencodeMessageContent(assistantMessage.info, assistantMessage.parts) ||
321
+ "[assistant response pending; check streamed parts or wait for session sync]";
322
+
323
+ const createdAt =
324
+ assistantMessage.info.time?.completed ??
325
+ assistantMessage.info.time?.created ??
326
+ Date.now();
327
+ const result = appendChatExchange({
328
+ sessionId: session.id,
329
+ userContent: this.summarizeUserInputForStorage(input.content, inputParts),
330
+ assistantContent: assistantText,
331
+ assistantParts,
332
+ source: "runtime",
333
+ createdAt,
334
+ userMessageId: assistantMessage.info.parentID,
335
+ assistantMessageId: assistantMessage.info.id,
336
+ usage: {
337
+ providerId: assistantMessage.info.providerID ?? null,
338
+ modelId: assistantMessage.info.modelID ?? null,
339
+ requestCountDelta: 1,
340
+ inputTokensDelta:
341
+ assistantMessage.info.tokens?.input ??
342
+ Math.max(8, promptInput.content.length * 2),
343
+ outputTokensDelta:
344
+ assistantMessage.info.tokens?.output ??
345
+ Math.max(24, Math.floor(promptInput.content.length * 2.5)),
346
+ estimatedCostUsdDelta: assistantMessage.info.cost ?? 0,
347
+ },
348
+ });
349
+ if (!result) throw new RuntimeSessionNotFoundError(input.sessionId);
350
+
351
+ if (trace) {
352
+ setMessageMemoryTrace({
353
+ sessionId: session.id,
354
+ messageId: assistantMessage.info.id,
355
+ trace,
356
+ createdAt,
357
+ });
358
+ for (const message of result.messages) {
359
+ if (message.id === assistantMessage.info.id && message.role === "assistant") {
360
+ message.memoryTrace = trace;
361
+ }
362
+ }
363
+ }
364
+
365
+ if (assistantParts.length > 0) {
366
+ for (const message of result.messages) {
367
+ if (message.id === assistantMessage.info.id && message.role === "assistant") {
368
+ message.parts = assistantParts;
369
+ }
370
+ }
371
+ }
372
+
373
+ for (const message of result.messages) {
374
+ this.emit(
375
+ createSessionMessageCreatedEvent(
376
+ { sessionId: result.session.id, message },
377
+ "runtime",
378
+ ),
379
+ );
380
+ }
381
+ this.emit(createSessionStateUpdatedEvent(result.session, "runtime"));
382
+ this.emit(createUsageUpdatedEvent(result.usage, "runtime"));
383
+ this.emit(createHeartbeatUpdatedEvent(result.heartbeat, "runtime"));
384
+ return { sessionId: result.session.id, messages: result.messages };
385
+ } finally {
386
+ try {
387
+ const queue = getLaneQueue();
388
+ if (queue.depth(session.id) > 0 && this.inFlightBackgroundChildRunCount(session.id) === 0) {
389
+ this["drainingSessions"].add(session.id);
390
+ this["busySessions"].delete(session.id);
391
+ while (queue.depth(session.id) > 0) {
392
+ try {
393
+ await queue.drainAndExecute(session.id);
394
+ } catch (err) {
395
+ console.error("Queue drain error:", err);
396
+ break;
397
+ }
398
+ }
399
+ }
400
+ } catch {
401
+ // Queue not initialized, ignore
402
+ } finally {
403
+ this["drainingSessions"].delete(session.id);
404
+ this["busySessions"].delete(session.id);
405
+ }
406
+ }
407
+ },
408
+
409
+ async spawnBackgroundSession(this: OpencodeRuntime, input) {
410
+ const parentSessionId = input.parentSessionId.trim();
411
+ const parentSession = getSessionById(parentSessionId);
412
+ if (!parentSession) throw new RuntimeSessionNotFoundError(parentSessionId);
413
+ await this.ensureRuntimeConfigSynced();
414
+ const parentOpencodeSessionId = await this.resolveOrCreateOpencodeSession(
415
+ parentSession.id,
416
+ parentSession.title,
417
+ );
418
+ const created = unwrapSdkData<Session>(
419
+ await this.getClient().session.create({
420
+ body: {
421
+ parentID: parentOpencodeSessionId,
422
+ title: input.title?.trim() || `${parentSession.title} background`,
423
+ },
424
+ responseStyle: "data",
425
+ throwOnError: true,
426
+ signal: this.defaultRequestSignal(),
427
+ }),
428
+ );
429
+ const run = createBackgroundRun({
430
+ runtime: OPENCODE_RUNTIME_ID,
431
+ parentSessionId: parentSession.id,
432
+ parentExternalSessionId: parentOpencodeSessionId,
433
+ childExternalSessionId: created.id,
434
+ requestedBy: input.requestedBy,
435
+ prompt: input.prompt,
436
+ status: "created",
437
+ });
438
+ if (!run) throw new Error("Failed to create background run record.");
439
+ this.ensureLocalSessionForBackgroundRun(run, created);
440
+ this.emitBackgroundRunUpdated(run);
441
+ return this.backgroundRecordToHandle(run);
442
+ },
443
+
444
+ async promptBackgroundAsync(this: OpencodeRuntime, input) {
445
+ const runId = input.runId.trim();
446
+ const content = input.content.trim();
447
+ const inputParts = this.normalizePromptInputParts(content, input.parts);
448
+ if (!runId) throw new Error("runId is required.");
449
+ if (!content && inputParts.length === 0) throw new Error("content or parts is required.");
450
+ const run = getBackgroundRunById(runId);
451
+ if (!run) throw new Error(`Unknown background run: ${runId}`);
452
+ const parentSession = getSessionById(run.parentSessionId);
453
+ if (!parentSession) throw new RuntimeSessionNotFoundError(run.parentSessionId);
454
+ await this.ensureRuntimeConfigSynced();
455
+ const model = this.resolveModel(input.model?.trim() || parentSession.model);
456
+ const requestedAgent = await this.resolveRequestedAgentId(input.agent?.trim());
457
+ const effectiveAgent =
458
+ requestedAgent ??
459
+ (await this.resolvePrimaryAgentId(undefined, { emitRetryStatus: false }));
460
+ const startedAt = run.startedAt ? undefined : Date.now();
461
+ const running =
462
+ setBackgroundRunStatus({
463
+ runId: run.id,
464
+ status: "running",
465
+ prompt: content,
466
+ startedAt,
467
+ completedAt: null,
468
+ resultSummary: null,
469
+ error: null,
470
+ }) ?? run;
471
+ this.emitBackgroundRunUpdated(running);
472
+ try {
473
+ await this.getClient().session.promptAsync({
474
+ path: { id: run.childExternalSessionId },
475
+ body: {
476
+ model: { providerID: model.providerId, modelID: model.modelId },
477
+ system: input.system,
478
+ agent: effectiveAgent,
479
+ noReply: input.noReply,
480
+ parts: inputParts,
481
+ },
482
+ responseStyle: "data",
483
+ throwOnError: true,
484
+ signal: this.promptRequestSignal(),
485
+ });
486
+ } catch (error) {
487
+ const normalizedError = this.normalizeRuntimeError(error);
488
+ const failed =
489
+ setBackgroundRunStatus({
490
+ runId: run.id,
491
+ status: "failed",
492
+ completedAt: Date.now(),
493
+ error: normalizedError.message,
494
+ }) ?? run;
495
+ this.emitBackgroundRunUpdated(failed);
496
+ throw normalizedError;
497
+ }
498
+ const refreshed = await this.getBackgroundStatus(run.id);
499
+ if (!refreshed) throw new Error(`Background run disappeared after dispatch: ${run.id}`);
500
+ return refreshed;
501
+ },
502
+
503
+ async getBackgroundStatus(this: OpencodeRuntime, runId) {
504
+ const normalizedRunId = runId.trim();
505
+ if (!normalizedRunId) return null;
506
+ let run = getBackgroundRunById(normalizedRunId);
507
+ if (!run) return null;
508
+ try {
509
+ const statuses = unwrapSdkData<Record<string, OpencodeSessionStatus>>(
510
+ await this.getClient().session.status({
511
+ responseStyle: "data",
512
+ throwOnError: true,
513
+ signal: this.defaultRequestSignal(),
514
+ }),
515
+ );
516
+ const opencodeStatus = statuses[run.childExternalSessionId];
517
+ if (opencodeStatus) {
518
+ run = this.applyOpencodeBackgroundStatus(run, opencodeStatus);
519
+ }
520
+ } catch {
521
+ // Keep local status if status refresh fails.
522
+ }
523
+ if (run.status === "completed") {
524
+ await this.announceBackgroundRunIfNeeded(run.id);
525
+ const refreshed = getBackgroundRunById(run.id);
526
+ if (refreshed) run = refreshed;
527
+ }
528
+ await this.syncBackgroundSessionMessages(
529
+ run,
530
+ run.status === "completed" || run.status === "failed" || run.status === "aborted",
531
+ );
532
+ return this.backgroundRecordToHandle(run);
533
+ },
534
+
535
+ async listBackgroundRuns(this: OpencodeRuntime, input) {
536
+ const limit = Math.max(1, Math.min(500, Math.floor(input?.limit ?? 100)));
537
+ const parentSessionId = input?.parentSessionId?.trim();
538
+ const runs = parentSessionId
539
+ ? listBackgroundRunsForParentSession(parentSessionId, limit)
540
+ : input?.inFlightOnly
541
+ ? listInFlightBackgroundRuns(OPENCODE_RUNTIME_ID, limit)
542
+ : listRecentBackgroundRuns(OPENCODE_RUNTIME_ID, limit);
543
+ const handles: Array<BackgroundRunHandle> = [];
544
+ for (const run of runs) {
545
+ const status = await this.getBackgroundStatus(run.id);
546
+ if (!status) continue;
547
+ handles.push(status);
548
+ }
549
+ return handles;
550
+ },
551
+
552
+ async abortBackground(this: OpencodeRuntime, runId) {
553
+ const normalizedRunId = runId.trim();
554
+ if (!normalizedRunId) return false;
555
+ const run = getBackgroundRunById(normalizedRunId);
556
+ if (!run) return false;
557
+ try {
558
+ const result = await this.getClient().session.abort({
559
+ path: { id: run.childExternalSessionId },
560
+ responseStyle: "data",
561
+ throwOnError: true,
562
+ signal: this.defaultRequestSignal(),
563
+ });
564
+ const aborted = Boolean(unwrapSdkData<boolean>(result));
565
+ if (aborted) {
566
+ const updated =
567
+ setBackgroundRunStatus({
568
+ runId: run.id,
569
+ status: "aborted",
570
+ completedAt: Date.now(),
571
+ error: null,
572
+ }) ?? run;
573
+ this.emitBackgroundRunUpdated(updated);
574
+ }
575
+ return aborted;
576
+ } catch (error) {
577
+ if (getOpencodeErrorStatus(error) === 404) return false;
578
+ throw this.normalizeRuntimeError(error);
579
+ }
580
+ },
581
+
582
+ async abortSession(this: OpencodeRuntime, sessionId) {
583
+ const opencodeSessionId = getRuntimeSessionBinding(OPENCODE_RUNTIME_ID, sessionId);
584
+ if (!opencodeSessionId) return false;
585
+ try {
586
+ const result = await this.getClient().session.abort({
587
+ path: { id: opencodeSessionId },
588
+ responseStyle: "data",
589
+ throwOnError: true,
590
+ signal: this.defaultRequestSignal(),
591
+ });
592
+ return Boolean(unwrapSdkData<boolean>(result));
593
+ } catch (error) {
594
+ if (getOpencodeErrorStatus(error) === 404) return false;
595
+ throw this.normalizeRuntimeError(error);
596
+ }
597
+ },
598
+
599
+ async compactSession(this: OpencodeRuntime, sessionId) {
600
+ const session = getSessionById(sessionId);
601
+ if (!session) throw new RuntimeSessionNotFoundError(sessionId);
602
+ let opencodeSessionId = await this.resolveOrCreateOpencodeSession(session.id, session.title);
603
+ const model = this.resolveModel(session.model);
604
+ try {
605
+ const result = await this.getClient().session.summarize({
606
+ path: { id: opencodeSessionId },
607
+ body: { providerID: model.providerId, modelID: model.modelId },
608
+ responseStyle: "data",
609
+ throwOnError: true,
610
+ signal: this.defaultRequestSignal(),
611
+ });
612
+ this.markMemoryInjectionStateForReinject(opencodeSessionId);
613
+ return Boolean(unwrapSdkData<boolean>(result));
614
+ } catch (error) {
615
+ if (getOpencodeErrorStatus(error) === 404) {
616
+ opencodeSessionId = await this.createOpencodeSession(session.id, session.title);
617
+ const retry = await this.getClient().session.summarize({
618
+ path: { id: opencodeSessionId },
619
+ body: { providerID: model.providerId, modelID: model.modelId },
620
+ responseStyle: "data",
621
+ throwOnError: true,
622
+ signal: this.defaultRequestSignal(),
623
+ });
624
+ this.markMemoryInjectionStateForReinject(opencodeSessionId);
625
+ return Boolean(unwrapSdkData<boolean>(retry));
626
+ }
627
+ throw this.normalizeRuntimeError(error);
628
+ }
629
+ },
630
+
631
+ async modelSupportsImageInput(this: OpencodeRuntime, model) {
632
+ const now = Date.now();
633
+ if (now - this["imageCapabilityFetchedAtMs"] > 60_000) {
634
+ this["imageCapabilityByModelRef"].clear();
635
+ }
636
+ const modelRef = this.formatModelRef(model);
637
+ if (
638
+ this["imageCapabilityByModelRef"].size > 0 &&
639
+ this["imageCapabilityByModelRef"].has(modelRef)
640
+ ) {
641
+ return this["imageCapabilityByModelRef"].get(modelRef) === true;
642
+ }
643
+ try {
644
+ const payload = unwrapSdkData<Record<string, unknown>>(
645
+ await this.getClient().config.providers({
646
+ responseStyle: "data",
647
+ throwOnError: true,
648
+ signal: this.defaultRequestSignal(),
649
+ }),
650
+ );
651
+ const map = new Map<string, boolean>();
652
+ const providers = Array.isArray(payload.providers) ? payload.providers : [];
653
+ for (const provider of providers) {
654
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) continue;
655
+ const providerRecord = provider as Record<string, unknown>;
656
+ const providerId =
657
+ typeof providerRecord.id === "string" ? providerRecord.id.trim() : "";
658
+ if (!providerId) continue;
659
+ const models =
660
+ providerRecord.models &&
661
+ typeof providerRecord.models === "object" &&
662
+ !Array.isArray(providerRecord.models)
663
+ ? (providerRecord.models as Record<string, Record<string, unknown>>)
664
+ : {};
665
+ for (const [modelKey, modelInfo] of Object.entries(models)) {
666
+ const modelIdRaw = typeof modelInfo.id === "string" ? modelInfo.id : modelKey;
667
+ const modelId = modelIdRaw.trim();
668
+ if (!modelId) continue;
669
+ const capabilities =
670
+ modelInfo.capabilities &&
671
+ typeof modelInfo.capabilities === "object" &&
672
+ !Array.isArray(modelInfo.capabilities)
673
+ ? (modelInfo.capabilities as Record<string, unknown>)
674
+ : {};
675
+ const input =
676
+ capabilities.input &&
677
+ typeof capabilities.input === "object" &&
678
+ !Array.isArray(capabilities.input)
679
+ ? (capabilities.input as Record<string, unknown>)
680
+ : {};
681
+ map.set(`${providerId}/${modelId}`, input.image === true);
682
+ }
683
+ }
684
+ this["imageCapabilityByModelRef"] = map;
685
+ this["imageCapabilityFetchedAtMs"] = now;
686
+ } catch {
687
+ return false;
688
+ }
689
+ return this["imageCapabilityByModelRef"].get(modelRef) === true;
690
+ },
691
+
692
+ currentImageModel(this: OpencodeRuntime) {
693
+ const runtimeConfig = this.currentRuntimeConfig();
694
+ const explicit = runtimeConfig?.imageModel?.trim();
695
+ if (explicit) return explicit;
696
+ return runtimeConfig?.fallbackModels.find((model: string) => model.trim())?.trim() || this.currentSmallModel();
697
+ },
698
+
699
+ emit(this: OpencodeRuntime, event) {
700
+ if (this["disposed"]) return;
701
+ for (const listener of this["listeners"]) {
702
+ listener(event);
703
+ }
704
+ },
705
+ };