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,1167 @@
1
+ import {
2
+ AGENT_NAME_CACHE_TTL_MS,
3
+ BUILTIN_PRIMARY_AGENT_IDS,
4
+ BUILTIN_SUBAGENT_IDS,
5
+ RUNTIME_HEALTH_CACHE_TTL_MS,
6
+ RUNTIME_HEALTH_OK_PATTERN,
7
+ RUNTIME_HEALTH_PROMPT,
8
+ RUNTIME_HEALTH_TIMEOUT_CAP_MS,
9
+ isPlainObject,
10
+ logger,
11
+ normalizeStringArray,
12
+ shallowEqualStringArrays,
13
+ type AssistantInfo,
14
+ type ChatMessagePart,
15
+ type Config,
16
+ type Part,
17
+ type ResolvedModel,
18
+ type RuntimeAgentCatalog,
19
+ type RuntimeHealthSnapshot,
20
+ type RuntimeInputPart,
21
+ type Message,
22
+ type Session,
23
+ } from "./shared";
24
+ import {
25
+ createSessionRunStatusUpdatedEvent,
26
+ createSessionStateUpdatedEvent,
27
+ } from "../../contracts/events";
28
+ import {
29
+ getSessionById,
30
+ getRuntimeSessionBinding,
31
+ setRuntimeSessionBinding,
32
+ setSessionTitle,
33
+ } from "../../db/repository";
34
+ import { isActiveHeartbeatSession } from "../../heartbeat/state";
35
+ import { getOpencodeErrorStatus, unwrapSdkData } from "../../opencode/client";
36
+ import {
37
+ buildManagedSkillPaths,
38
+ getManagedSkillsRootPath,
39
+ } from "../../skills/service";
40
+ import {
41
+ RuntimeProviderAuthError,
42
+ RuntimeProviderQuotaError,
43
+ RuntimeProviderRateLimitError,
44
+ } from "../errors";
45
+ import type { OpencodeRuntime } from "../opencodeRuntime";
46
+
47
+ export interface OpencodeRuntimePromptMethods {
48
+ resolveModel(rawModel: string): ResolvedModel;
49
+ extractText(parts: Array<Part>): string | null;
50
+ extractReasoningText(parts: Array<Part>): string | null;
51
+ extractCompletedToolOutputText(parts: Array<Part>): string | null;
52
+ extractSubtaskPrompt(parts: Array<Part>): string | null;
53
+ summarizeBackgroundResult(text: string | null): string;
54
+ mapChatMessagePart(part: Part): ChatMessagePart | null;
55
+ buildChatMessageParts(parts: Array<Part>): ChatMessagePart[];
56
+ extractAssistantError(info: AssistantInfo, parts: Array<Part>): string | null;
57
+ describeUnknownError(error: unknown): string | null;
58
+ sendPrompt(
59
+ sessionId: string,
60
+ model: ResolvedModel,
61
+ promptParts: Array<RuntimeInputPart>,
62
+ system?: string,
63
+ agent?: string,
64
+ ): Promise<{ info: AssistantInfo; parts: Array<Part> }>;
65
+ sendPromptWithModelFallback(input: {
66
+ localSessionId: string;
67
+ localSessionTitle: string;
68
+ opencodeSessionId: string;
69
+ primaryModel: ResolvedModel;
70
+ parts: Array<RuntimeInputPart>;
71
+ retryPartsOnSessionRecreate?: Array<RuntimeInputPart>;
72
+ memoryContextFingerprint?: string | null;
73
+ system?: string;
74
+ agent?: string;
75
+ }): Promise<{
76
+ message: { info: AssistantInfo; parts: Array<Part> };
77
+ opencodeSessionId: string;
78
+ }>;
79
+ sendPromptWithAgentFallback(input: {
80
+ localSessionId?: string;
81
+ sessionId: string;
82
+ model: ResolvedModel;
83
+ parts: Array<RuntimeInputPart>;
84
+ system?: string;
85
+ agent?: string;
86
+ }): Promise<{ info: AssistantInfo; parts: Array<Part> }>;
87
+ resolvePromptModels(primaryModel: ResolvedModel): Array<ResolvedModel>;
88
+ formatModelRef(model: ResolvedModel): string;
89
+ emitPromptRetryStatus(
90
+ sessionId: string,
91
+ attempt: number,
92
+ error: unknown,
93
+ previousModel: ResolvedModel | null,
94
+ nextModel: ResolvedModel,
95
+ ): void;
96
+ extractErrorStatusCode(error: unknown): number | null;
97
+ isModelNotFoundError(error: unknown): boolean;
98
+ resolveOrCreateOpencodeSession(
99
+ localSessionId: string,
100
+ localTitle: string,
101
+ ): Promise<string>;
102
+ createOpencodeSession(
103
+ localSessionId: string,
104
+ localTitle: string,
105
+ ): Promise<string>;
106
+ isPlaceholderSessionTitle(title: string): boolean;
107
+ startSessionTitlePolling(
108
+ localSessionId: string,
109
+ opencodeSessionId: string,
110
+ ): void;
111
+ syncSessionTitleFromOpencode(
112
+ localSessionId: string,
113
+ opencodeSessionId: string,
114
+ localTitle: string,
115
+ emitUpdateEvent?: boolean,
116
+ ): Promise<boolean | undefined>;
117
+ normalizeRuntimeError(error: unknown): Error;
118
+ normalizeProviderMessage(message: unknown): string;
119
+ categorizeProviderError(status: number | null, message: string): Error | null;
120
+ shouldFailoverPromptError(error: unknown): boolean;
121
+ isTimeoutLikeError(error: unknown): boolean;
122
+ isInvalidAgentPromptError(error: unknown): boolean;
123
+ resolveRequestedAgentId(
124
+ agent: string | undefined,
125
+ sessionId?: string,
126
+ ): Promise<string | undefined>;
127
+ agentModeFromConfig(
128
+ agentId: string,
129
+ config: Record<string, unknown>,
130
+ ): "subagent" | "primary" | "all";
131
+ resolvePrimaryAgentId(
132
+ sessionId?: string,
133
+ options?: { emitRetryStatus?: boolean },
134
+ ): Promise<string | undefined>;
135
+ fetchAvailableAgentNames(): Promise<Set<string> | null>;
136
+ fetchAvailableAgentCatalog(): Promise<RuntimeAgentCatalog | null>;
137
+ defaultRequestSignal(): AbortSignal;
138
+ promptRequestSignal(): AbortSignal;
139
+ healthProbeSignal(timeoutMs: number): AbortSignal;
140
+ healthProbeTimeoutMs(): number;
141
+ waitFor(ms: number): Promise<void>;
142
+ clearAllTimers(timers: Map<string, ReturnType<typeof setTimeout>>): void;
143
+ normalizeHealthProbeError(error: unknown, timeoutMs: number): Error;
144
+ runHealthProbe(): Promise<RuntimeHealthSnapshot>;
145
+ runtimeConfigTargetKey(): string;
146
+ ensureRuntimeConfigSynced(force?: boolean): Promise<void>;
147
+ applyRuntimeConfigSync(targetKey: string): Promise<void>;
148
+ }
149
+
150
+ export const opencodeRuntimePromptMethods: OpencodeRuntimePromptMethods = {
151
+ resolveModel(this: OpencodeRuntime, rawModel) {
152
+ const trimmed = rawModel.trim();
153
+ if (trimmed.includes("/")) {
154
+ const [providerId, ...rest] = trimmed.split("/");
155
+ const modelId = rest.join("/").trim();
156
+ if (providerId && modelId) {
157
+ return { providerId, modelId };
158
+ }
159
+ }
160
+ return {
161
+ providerId: this.currentProviderId(),
162
+ modelId: trimmed || this.currentModelId(),
163
+ };
164
+ },
165
+
166
+ extractText(this: OpencodeRuntime, parts: Array<Part>) {
167
+ const text = parts
168
+ .filter(
169
+ (part): part is Extract<Part, { type: "text" }> => part.type === "text",
170
+ )
171
+ .map((part) => part.text.trim())
172
+ .filter(Boolean)
173
+ .join("\n\n");
174
+ return text || null;
175
+ },
176
+
177
+ extractReasoningText(this: OpencodeRuntime, parts: Array<Part>) {
178
+ const text = parts
179
+ .filter(
180
+ (part): part is Extract<Part, { type: "reasoning" }> =>
181
+ part.type === "reasoning",
182
+ )
183
+ .map((part) => part.text.trim())
184
+ .filter(Boolean)
185
+ .join("\n\n");
186
+ return text || null;
187
+ },
188
+
189
+ extractCompletedToolOutputText(this: OpencodeRuntime, parts: Array<Part>) {
190
+ const outputs: string[] = [];
191
+ for (const part of parts) {
192
+ if (part.type !== "tool") continue;
193
+ if (part.state.status !== "completed") continue;
194
+ const output = part.state.output.trim();
195
+ if (output) outputs.push(output);
196
+ }
197
+ return outputs.length > 0 ? outputs.join("\n\n") : null;
198
+ },
199
+
200
+ extractSubtaskPrompt(this: OpencodeRuntime, parts: Array<Part>) {
201
+ const subtask = parts.find(
202
+ (part): part is Extract<Part, { type: "subtask" }> =>
203
+ part.type === "subtask",
204
+ );
205
+ const prompt = subtask?.prompt.trim();
206
+ return prompt || null;
207
+ },
208
+
209
+ summarizeBackgroundResult(this: OpencodeRuntime, text: string | null) {
210
+ const normalized = (text ?? "").replace(/\s+/g, " ").trim();
211
+ if (!normalized) return "Background run completed.";
212
+ if (normalized.length <= 800) return normalized;
213
+ return `${normalized.slice(0, 800)}...`;
214
+ },
215
+
216
+ mapChatMessagePart(this: OpencodeRuntime, part) {
217
+ const toIsoIfFiniteMillis = (value: unknown): string | undefined => {
218
+ if (typeof value !== "number" || !Number.isFinite(value))
219
+ return undefined;
220
+ return new Date(value).toISOString();
221
+ };
222
+ if (part.type === "reasoning") {
223
+ const text = part.text.trim();
224
+ if (!text) return null;
225
+ return {
226
+ id: part.id,
227
+ type: "thinking",
228
+ text,
229
+ startedAt: toIsoIfFiniteMillis(part.time.start),
230
+ endedAt: toIsoIfFiniteMillis(part.time.end),
231
+ };
232
+ }
233
+ if (part.type !== "tool") return null;
234
+ const stateTime =
235
+ "time" in part.state &&
236
+ part.state.time &&
237
+ typeof part.state.time === "object"
238
+ ? part.state.time
239
+ : null;
240
+ const startedAt =
241
+ stateTime && "start" in stateTime
242
+ ? toIsoIfFiniteMillis(stateTime.start)
243
+ : undefined;
244
+ const endedAt =
245
+ stateTime && "end" in stateTime
246
+ ? toIsoIfFiniteMillis(stateTime.end)
247
+ : undefined;
248
+ const output =
249
+ part.state.status === "completed"
250
+ ? part.state.output.trim() || undefined
251
+ : undefined;
252
+ const error =
253
+ part.state.status === "error"
254
+ ? part.state.error.trim() || undefined
255
+ : undefined;
256
+ return {
257
+ id: part.id,
258
+ type: "tool_call",
259
+ toolCallId: part.callID,
260
+ tool: part.tool,
261
+ status: part.state.status,
262
+ input: isPlainObject(part.state.input) ? part.state.input : undefined,
263
+ output,
264
+ error,
265
+ startedAt,
266
+ endedAt,
267
+ };
268
+ },
269
+
270
+ buildChatMessageParts(this: OpencodeRuntime, parts) {
271
+ const mapped = parts
272
+ .map((part) => this.mapChatMessagePart(part))
273
+ .filter((part): part is ChatMessagePart => Boolean(part));
274
+ if (mapped.length === 0) return [];
275
+ const deduped = new Map<string, ChatMessagePart>();
276
+ for (const part of mapped) {
277
+ deduped.set(part.id, part);
278
+ }
279
+ return [...deduped.values()];
280
+ },
281
+
282
+ extractAssistantError(this: OpencodeRuntime, info, parts) {
283
+ const infoError = this.describeUnknownError(info.error);
284
+ if (infoError) return infoError;
285
+ for (const part of parts) {
286
+ if (part.type !== "tool" || part.state.status !== "error") continue;
287
+ const reason = part.state.error?.trim();
288
+ if (reason) return `Tool ${part.tool} failed: ${reason}`;
289
+ return `Tool ${part.tool} failed.`;
290
+ }
291
+ return null;
292
+ },
293
+
294
+ describeUnknownError(this: OpencodeRuntime, error: unknown) {
295
+ if (!error) return null;
296
+ if (typeof error === "string") {
297
+ const trimmed = error.trim();
298
+ return trimmed || null;
299
+ }
300
+ if (error instanceof Error) {
301
+ const message = error.message.trim();
302
+ return message || error.name || "Unknown error";
303
+ }
304
+ if (typeof error !== "object") return String(error);
305
+ const record = error as Record<string, unknown>;
306
+ const directMessage =
307
+ typeof record.message === "string" ? record.message.trim() : "";
308
+ if (directMessage) return directMessage;
309
+ const dataMessage =
310
+ record.data &&
311
+ typeof record.data === "object" &&
312
+ typeof (record.data as Record<string, unknown>).message === "string"
313
+ ? ((record.data as Record<string, unknown>).message as string).trim()
314
+ : "";
315
+ if (dataMessage) return dataMessage;
316
+ const name = typeof record.name === "string" ? record.name.trim() : "";
317
+ if (name) return name;
318
+ return null;
319
+ },
320
+
321
+ async sendPrompt(
322
+ this: OpencodeRuntime,
323
+ sessionId,
324
+ model,
325
+ promptParts,
326
+ system,
327
+ agent,
328
+ ) {
329
+ const response = unwrapSdkData<{ info?: Message; parts?: Array<Part> }>(
330
+ await this.getClient().session.prompt({
331
+ path: { id: sessionId },
332
+ body: {
333
+ model: { providerID: model.providerId, modelID: model.modelId },
334
+ system,
335
+ agent,
336
+ parts: promptParts,
337
+ },
338
+ responseStyle: "data",
339
+ throwOnError: true,
340
+ signal: this.promptRequestSignal(),
341
+ }),
342
+ );
343
+ if (!response || typeof response !== "object") {
344
+ throw new Error("OpenCode returned an empty prompt response.");
345
+ }
346
+ const info = response.info;
347
+ if (!info || typeof info !== "object") {
348
+ const topLevelError =
349
+ this.describeUnknownError(
350
+ (response as Record<string, unknown>).error,
351
+ ) ?? this.describeUnknownError(response);
352
+ if (topLevelError) {
353
+ const wrapped = new Error(topLevelError);
354
+ const status = this.extractErrorStatusCode(
355
+ (response as Record<string, unknown>).error ?? response,
356
+ );
357
+ if (typeof status === "number") {
358
+ (wrapped as Error & { status?: number }).status = status;
359
+ }
360
+ throw wrapped;
361
+ }
362
+ throw new Error("OpenCode prompt response is missing message metadata.");
363
+ }
364
+ const infoRecord = info as Record<string, unknown>;
365
+ const infoError = this.describeUnknownError(infoRecord.error);
366
+ if (infoError) {
367
+ const wrapped = new Error(infoError);
368
+ const status = this.extractErrorStatusCode(infoRecord.error);
369
+ if (typeof status === "number") {
370
+ (wrapped as Error & { status?: number }).status = status;
371
+ }
372
+ throw wrapped;
373
+ }
374
+ if (info.role !== "assistant") {
375
+ throw new Error(
376
+ `OpenCode returned unexpected message role: ${info.role}`,
377
+ );
378
+ }
379
+ return {
380
+ info: info as AssistantInfo,
381
+ parts: Array.isArray(response.parts) ? response.parts : [],
382
+ };
383
+ },
384
+
385
+ async sendPromptWithModelFallback(this: OpencodeRuntime, input) {
386
+ const models = this.resolvePromptModels(input.primaryModel);
387
+ let sessionId = input.opencodeSessionId;
388
+ let previousError: unknown = null;
389
+
390
+ for (let index = 0; index < models.length; index += 1) {
391
+ const model = models[index];
392
+ if (!model) continue;
393
+ if (index > 0) {
394
+ this.emitPromptRetryStatus(
395
+ input.localSessionId,
396
+ index + 1,
397
+ previousError,
398
+ models[index - 1] ?? null,
399
+ model,
400
+ );
401
+ }
402
+
403
+ let attemptError: unknown = null;
404
+ try {
405
+ const message = await this.sendPromptWithAgentFallback({
406
+ localSessionId: input.localSessionId,
407
+ sessionId,
408
+ model,
409
+ parts: input.parts,
410
+ system: input.system,
411
+ agent: input.agent,
412
+ });
413
+ return { message, opencodeSessionId: sessionId };
414
+ } catch (error) {
415
+ if (getOpencodeErrorStatus(error) === 404) {
416
+ const previousSessionState = this.getMemoryInjectionState(sessionId);
417
+ try {
418
+ sessionId = await this.createOpencodeSession(
419
+ input.localSessionId,
420
+ input.localSessionTitle,
421
+ );
422
+ } catch (createError) {
423
+ throw this.normalizeRuntimeError(createError);
424
+ }
425
+ try {
426
+ if (input.memoryContextFingerprint) {
427
+ this.setMemoryInjectionState(sessionId, {
428
+ fingerprint: input.memoryContextFingerprint,
429
+ forceReinject: false,
430
+ generation: previousSessionState?.generation ?? 0,
431
+ turn: previousSessionState?.turn ?? 0,
432
+ injectedKeysByGeneration: [
433
+ ...(previousSessionState?.injectedKeysByGeneration ?? []),
434
+ ],
435
+ });
436
+ } else if (previousSessionState) {
437
+ this.setMemoryInjectionState(sessionId, {
438
+ ...previousSessionState,
439
+ });
440
+ }
441
+ const retryParts = input.retryPartsOnSessionRecreate ?? input.parts;
442
+ const message = await this.sendPromptWithAgentFallback({
443
+ localSessionId: input.localSessionId,
444
+ sessionId,
445
+ model,
446
+ parts: retryParts,
447
+ system: input.system,
448
+ agent: input.agent,
449
+ });
450
+ return { message, opencodeSessionId: sessionId };
451
+ } catch (retryError) {
452
+ attemptError = retryError;
453
+ }
454
+ } else {
455
+ attemptError = error;
456
+ }
457
+ }
458
+
459
+ previousError = attemptError;
460
+ const hasMoreModels = index < models.length - 1;
461
+ if (!hasMoreModels) {
462
+ throw this.normalizeRuntimeError(attemptError);
463
+ }
464
+ if (
465
+ !(
466
+ this.isModelNotFoundError(attemptError) ||
467
+ this.shouldFailoverPromptError(attemptError)
468
+ )
469
+ ) {
470
+ throw this.normalizeRuntimeError(attemptError);
471
+ }
472
+ }
473
+
474
+ throw this.normalizeRuntimeError(previousError);
475
+ },
476
+
477
+ async sendPromptWithAgentFallback(this: OpencodeRuntime, input) {
478
+ try {
479
+ return await this.sendPrompt(
480
+ input.sessionId,
481
+ input.model,
482
+ input.parts,
483
+ input.system,
484
+ input.agent,
485
+ );
486
+ } catch (error) {
487
+ if (!this.isInvalidAgentPromptError(error)) throw error;
488
+ if (input.agent) {
489
+ try {
490
+ return await this.sendPrompt(
491
+ input.sessionId,
492
+ input.model,
493
+ input.parts,
494
+ input.system,
495
+ undefined,
496
+ );
497
+ } catch (fallbackError) {
498
+ if (!this.isInvalidAgentPromptError(fallbackError))
499
+ throw fallbackError;
500
+ const primaryAgent = await this.resolvePrimaryAgentId(
501
+ input.localSessionId,
502
+ );
503
+ if (!primaryAgent) throw fallbackError;
504
+ return this.sendPrompt(
505
+ input.sessionId,
506
+ input.model,
507
+ input.parts,
508
+ input.system,
509
+ primaryAgent,
510
+ );
511
+ }
512
+ }
513
+ const fallbackAgent = await this.resolvePrimaryAgentId(
514
+ input.localSessionId,
515
+ );
516
+ if (!fallbackAgent) throw error;
517
+ return this.sendPrompt(
518
+ input.sessionId,
519
+ input.model,
520
+ input.parts,
521
+ input.system,
522
+ fallbackAgent,
523
+ );
524
+ }
525
+ },
526
+
527
+ resolvePromptModels(this: OpencodeRuntime, primaryModel) {
528
+ const models: Array<ResolvedModel> = [];
529
+ const seen = new Set<string>();
530
+ const add = (model: ResolvedModel) => {
531
+ const key = this.formatModelRef(model);
532
+ if (seen.has(key)) return;
533
+ seen.add(key);
534
+ models.push(model);
535
+ };
536
+ add(primaryModel);
537
+ const fallbackRefs = this.currentFallbackModels();
538
+ for (const fallbackRef of fallbackRefs) {
539
+ add(this.resolveModel(fallbackRef));
540
+ }
541
+ if (fallbackRefs.length === 0) {
542
+ add({
543
+ providerId: this.currentProviderId(),
544
+ modelId: this.currentModelId(),
545
+ });
546
+ }
547
+ return models;
548
+ },
549
+
550
+ formatModelRef(this: OpencodeRuntime, model: ResolvedModel) {
551
+ return `${model.providerId}/${model.modelId}`;
552
+ },
553
+
554
+ emitPromptRetryStatus(
555
+ this: OpencodeRuntime,
556
+ sessionId,
557
+ attempt,
558
+ error,
559
+ previousModel,
560
+ nextModel,
561
+ ) {
562
+ const detail = this.normalizeRuntimeError(error).message;
563
+ const nextRef = this.formatModelRef(nextModel);
564
+ const message = this.isModelNotFoundError(error)
565
+ ? `Model ${previousModel ? this.formatModelRef(previousModel) : "requested"} is not available at the selected provider. Retrying with ${nextRef}.`
566
+ : `${detail} Retrying with ${nextRef}.`;
567
+ this.emit(
568
+ createSessionRunStatusUpdatedEvent(
569
+ { sessionId, status: "retry", attempt, message },
570
+ "runtime",
571
+ ),
572
+ );
573
+ },
574
+
575
+ extractErrorStatusCode(this: OpencodeRuntime, error: unknown) {
576
+ if (!error || typeof error !== "object") return null;
577
+ const record = error as Record<string, unknown>;
578
+ if (typeof record.status === "number" && Number.isFinite(record.status))
579
+ return record.status;
580
+ if (
581
+ typeof record.statusCode === "number" &&
582
+ Number.isFinite(record.statusCode)
583
+ ) {
584
+ return record.statusCode;
585
+ }
586
+ const data = record.data;
587
+ if (data && typeof data === "object") {
588
+ const dataRecord = data as Record<string, unknown>;
589
+ if (
590
+ typeof dataRecord.statusCode === "number" &&
591
+ Number.isFinite(dataRecord.statusCode)
592
+ ) {
593
+ return dataRecord.statusCode;
594
+ }
595
+ }
596
+ return null;
597
+ },
598
+
599
+ isModelNotFoundError(this: OpencodeRuntime, error) {
600
+ const message = (this.describeUnknownError(error) ?? "").toLowerCase();
601
+ return message.includes("model not found");
602
+ },
603
+
604
+ async resolveOrCreateOpencodeSession(
605
+ this: OpencodeRuntime,
606
+ localSessionId,
607
+ localTitle,
608
+ ) {
609
+ const bound = getRuntimeSessionBinding("opencode", localSessionId);
610
+ if (bound) return bound;
611
+ return this.createOpencodeSession(localSessionId, localTitle);
612
+ },
613
+
614
+ async createOpencodeSession(
615
+ this: OpencodeRuntime,
616
+ localSessionId,
617
+ localTitle,
618
+ ) {
619
+ const body = localSessionId === "main" ? { title: localTitle } : {};
620
+ const created = unwrapSdkData<Session>(
621
+ await this.getClient().session.create({
622
+ body,
623
+ responseStyle: "data",
624
+ throwOnError: true,
625
+ signal: this.defaultRequestSignal(),
626
+ }),
627
+ );
628
+ setRuntimeSessionBinding("opencode", localSessionId, created.id);
629
+ return created.id;
630
+ },
631
+
632
+ isPlaceholderSessionTitle(this: OpencodeRuntime, title: string) {
633
+ const normalized = title.trim();
634
+ if (!normalized) return true;
635
+ return (
636
+ /^session \d+$/i.test(normalized) ||
637
+ /^new session(?:\s*-\s*.+)?$/i.test(normalized)
638
+ );
639
+ },
640
+
641
+ startSessionTitlePolling(
642
+ this: OpencodeRuntime,
643
+ localSessionId,
644
+ opencodeSessionId,
645
+ ) {
646
+ if (localSessionId === "main") return;
647
+ const localSession = getSessionById(localSessionId);
648
+ if (!localSession || !this.isPlaceholderSessionTitle(localSession.title))
649
+ return;
650
+ void (async () => {
651
+ const retryDelaysMs = [600, 1200, 2200, 3500, 5000, 7000];
652
+ for (const delayMs of retryDelaysMs) {
653
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
654
+ const currentLocal = getSessionById(localSessionId);
655
+ if (!currentLocal) return;
656
+ if (!this.isPlaceholderSessionTitle(currentLocal.title)) return;
657
+ const synced = await this.syncSessionTitleFromOpencode(
658
+ localSessionId,
659
+ opencodeSessionId,
660
+ currentLocal.title,
661
+ true,
662
+ );
663
+ if (synced) return;
664
+ }
665
+ })();
666
+ },
667
+
668
+ async syncSessionTitleFromOpencode(
669
+ this: OpencodeRuntime,
670
+ localSessionId,
671
+ opencodeSessionId,
672
+ localTitle,
673
+ emitUpdateEvent = false,
674
+ ) {
675
+ if (localSessionId === "main") return;
676
+ if (isActiveHeartbeatSession(localSessionId)) return false;
677
+ try {
678
+ const opencodeSession = unwrapSdkData<Session>(
679
+ await this.getClient().session.get({
680
+ path: { id: opencodeSessionId },
681
+ responseStyle: "data",
682
+ throwOnError: true,
683
+ signal: this.defaultRequestSignal(),
684
+ }),
685
+ );
686
+ const remoteTitle = opencodeSession.title.trim();
687
+ if (this.isPlaceholderSessionTitle(remoteTitle)) return false;
688
+ if (!remoteTitle || remoteTitle === localTitle) return false;
689
+ const updated = setSessionTitle(localSessionId, remoteTitle);
690
+ if (emitUpdateEvent && updated) {
691
+ this.emit(createSessionStateUpdatedEvent(updated, "runtime"));
692
+ }
693
+ return true;
694
+ } catch (error) {
695
+ logger.warnWithCause("Session title sync failed", error, {
696
+ localSessionId,
697
+ localTitle,
698
+ });
699
+ return false;
700
+ }
701
+ },
702
+
703
+ normalizeRuntimeError(this: OpencodeRuntime, error) {
704
+ const status = getOpencodeErrorStatus(error);
705
+ const fallback =
706
+ this.describeUnknownError(error) ?? "OpenCode request failed.";
707
+ const categorized = this.categorizeProviderError(status, fallback);
708
+ if (categorized) return categorized;
709
+ if (status !== null)
710
+ return new Error(`OpenCode API error (${status}): ${fallback}`);
711
+ if (error instanceof Error) return error;
712
+ return new Error(fallback);
713
+ },
714
+
715
+ normalizeProviderMessage(this: OpencodeRuntime, message) {
716
+ if (typeof message !== "string" || !message.trim()) return "";
717
+ const normalized = this.categorizeProviderError(null, message);
718
+ return normalized ? normalized.message : message;
719
+ },
720
+
721
+ categorizeProviderError(
722
+ this: OpencodeRuntime,
723
+ status: number | null,
724
+ message: string,
725
+ ) {
726
+ const normalized = message.toLowerCase();
727
+ const includes = (needle: string) => normalized.includes(needle);
728
+ const hasAny = (values: Array<string>) => values.some(includes);
729
+ if (
730
+ hasAny([
731
+ "exceeded_current_quota_error",
732
+ "exceeded your current token quota",
733
+ "freeusagelimiterror",
734
+ "insufficient_quota",
735
+ "quota exceeded",
736
+ "not enough credits",
737
+ "credit balance",
738
+ ])
739
+ ) {
740
+ return new RuntimeProviderQuotaError();
741
+ }
742
+ if (
743
+ status === 401 ||
744
+ status === 403 ||
745
+ hasAny([
746
+ "invalid api key",
747
+ "authentication failed",
748
+ "unauthorized",
749
+ "forbidden",
750
+ "auth error",
751
+ "bad credentials",
752
+ ])
753
+ ) {
754
+ return new RuntimeProviderAuthError();
755
+ }
756
+ if (
757
+ status === 429 ||
758
+ hasAny([
759
+ "too many requests",
760
+ "rate limited",
761
+ "rate limit",
762
+ "rate_limit",
763
+ "provider is overloaded",
764
+ ])
765
+ ) {
766
+ return new RuntimeProviderRateLimitError();
767
+ }
768
+ return null;
769
+ },
770
+
771
+ shouldFailoverPromptError(this: OpencodeRuntime, error) {
772
+ const status = getOpencodeErrorStatus(error);
773
+ if (status !== null) {
774
+ if ([401, 402, 403, 408, 429, 500, 502, 503, 504].includes(status))
775
+ return true;
776
+ if ([400, 404].includes(status)) return false;
777
+ }
778
+ const message = this.describeUnknownError(error) ?? "";
779
+ if (this.categorizeProviderError(status, message)) return true;
780
+ const normalized = message.toLowerCase();
781
+ const hasAny = (values: Array<string>) =>
782
+ values.some((value) => normalized.includes(value));
783
+ if (this.isTimeoutLikeError(error)) return true;
784
+ if (
785
+ hasAny([
786
+ "temporarily unavailable",
787
+ "provider is overloaded",
788
+ "upstream",
789
+ "network error",
790
+ "socket hang up",
791
+ "connection reset",
792
+ "econnreset",
793
+ ])
794
+ ) {
795
+ return true;
796
+ }
797
+ return false;
798
+ },
799
+
800
+ isTimeoutLikeError(this: OpencodeRuntime, error) {
801
+ if (error instanceof DOMException && error.name === "AbortError")
802
+ return true;
803
+ const message = this.describeUnknownError(error)?.toLowerCase() ?? "";
804
+ return Boolean(
805
+ message &&
806
+ (message.includes("timed out") ||
807
+ message.includes("timeout") ||
808
+ message.includes("operation timed out")),
809
+ );
810
+ },
811
+
812
+ isInvalidAgentPromptError(this: OpencodeRuntime, error) {
813
+ const normalized = (this.describeUnknownError(error) ?? "").toLowerCase();
814
+ if (!normalized) return false;
815
+ return (
816
+ normalized.includes("agent.variant") ||
817
+ (normalized.includes("default agent") &&
818
+ normalized.includes("not found")) ||
819
+ (normalized.includes("default agent") &&
820
+ normalized.includes("subagent")) ||
821
+ normalized.includes("is not an object")
822
+ );
823
+ },
824
+
825
+ async resolveRequestedAgentId(this: OpencodeRuntime, agent, sessionId) {
826
+ const normalized = agent?.trim();
827
+ if (!normalized) return undefined;
828
+ const names = await this.fetchAvailableAgentNames();
829
+ if (!names) return normalized;
830
+ if (names.has(normalized)) return normalized;
831
+ if (sessionId) {
832
+ this.emit(
833
+ createSessionRunStatusUpdatedEvent(
834
+ {
835
+ sessionId,
836
+ status: "retry",
837
+ attempt: 1,
838
+ message: `Requested agent "${normalized}" is unavailable in OpenCode. Falling back to default agent.`,
839
+ },
840
+ "runtime",
841
+ ),
842
+ );
843
+ }
844
+ return undefined;
845
+ },
846
+
847
+ agentModeFromConfig(
848
+ this: OpencodeRuntime,
849
+ agentId: string,
850
+ config: Record<string, unknown>,
851
+ ) {
852
+ const explicit = typeof config.mode === "string" ? config.mode.trim() : "";
853
+ if (
854
+ explicit === "subagent" ||
855
+ explicit === "primary" ||
856
+ explicit === "all"
857
+ ) {
858
+ return explicit;
859
+ }
860
+ if (BUILTIN_SUBAGENT_IDS.has(agentId)) return "subagent";
861
+ if (BUILTIN_PRIMARY_AGENT_IDS.has(agentId)) return "primary";
862
+ return "all";
863
+ },
864
+
865
+ async resolvePrimaryAgentId(this: OpencodeRuntime, sessionId, options) {
866
+ const catalog = await this.fetchAvailableAgentCatalog();
867
+ const selected = catalog?.primaryId;
868
+ if (!selected) return undefined;
869
+ if ((options?.emitRetryStatus ?? true) && sessionId) {
870
+ this.emit(
871
+ createSessionRunStatusUpdatedEvent(
872
+ {
873
+ sessionId,
874
+ status: "retry",
875
+ attempt: 1,
876
+ message: `OpenCode default agent is unavailable. Retrying with primary agent "${selected}" (by id).`,
877
+ },
878
+ "runtime",
879
+ ),
880
+ );
881
+ }
882
+ return selected;
883
+ },
884
+
885
+ async fetchAvailableAgentNames(this: OpencodeRuntime) {
886
+ const catalog = await this.fetchAvailableAgentCatalog();
887
+ return catalog?.ids ?? null;
888
+ },
889
+
890
+ async fetchAvailableAgentCatalog(this: OpencodeRuntime) {
891
+ const now = Date.now();
892
+ if (
893
+ this["availableAgentNamesCache"] &&
894
+ now - this["availableAgentNamesCache"].fetchedAtMs <=
895
+ AGENT_NAME_CACHE_TTL_MS
896
+ ) {
897
+ return this["availableAgentNamesCache"].catalog;
898
+ }
899
+ try {
900
+ const config = unwrapSdkData<Config>(
901
+ await this.getClient().config.get({
902
+ responseStyle: "data",
903
+ throwOnError: true,
904
+ signal: this.defaultRequestSignal(),
905
+ }),
906
+ );
907
+ const record = config as Record<string, unknown>;
908
+ const configuredAgentMap = isPlainObject(record.agent)
909
+ ? (record.agent as Record<string, unknown>)
910
+ : {};
911
+ const defaultAgentId =
912
+ typeof record.default_agent === "string"
913
+ ? record.default_agent.trim()
914
+ : "";
915
+ const ids = new Set<string>([
916
+ "build",
917
+ "plan",
918
+ "general",
919
+ "explore",
920
+ "title",
921
+ "summary",
922
+ "compaction",
923
+ ]);
924
+ let primaryId: string | undefined;
925
+ for (const [rawId, rawConfig] of Object.entries(configuredAgentMap)) {
926
+ const id = rawId.trim();
927
+ if (!id) continue;
928
+ ids.add(id);
929
+ if (!isPlainObject(rawConfig)) continue;
930
+ const disabled = rawConfig.disable === true;
931
+ if (disabled) continue;
932
+ const hidden = rawConfig.hidden === true;
933
+ const mode = this.agentModeFromConfig(id, rawConfig);
934
+ if (!primaryId && mode !== "subagent" && !hidden) {
935
+ primaryId = id;
936
+ }
937
+ }
938
+ if (defaultAgentId) {
939
+ const defaultConfig = isPlainObject(configuredAgentMap[defaultAgentId])
940
+ ? (configuredAgentMap[defaultAgentId] as Record<string, unknown>)
941
+ : null;
942
+ const disabled = defaultConfig?.disable === true;
943
+ const hidden = defaultConfig?.hidden === true;
944
+ const mode = defaultConfig
945
+ ? this.agentModeFromConfig(defaultAgentId, defaultConfig)
946
+ : "primary";
947
+ if (!disabled && !hidden && mode !== "subagent") {
948
+ primaryId = defaultAgentId;
949
+ }
950
+ }
951
+ if (!primaryId) {
952
+ const buildConfig = isPlainObject(configuredAgentMap.build)
953
+ ? (configuredAgentMap.build as Record<string, unknown>)
954
+ : null;
955
+ if (buildConfig?.disable !== true) {
956
+ primaryId = "build";
957
+ }
958
+ }
959
+ const catalog: RuntimeAgentCatalog = { ids, primaryId };
960
+ this["availableAgentNamesCache"] = { fetchedAtMs: now, catalog };
961
+ return catalog;
962
+ } catch {
963
+ return null;
964
+ }
965
+ },
966
+
967
+ defaultRequestSignal(this: OpencodeRuntime) {
968
+ return AbortSignal.timeout(this.currentTimeoutMs());
969
+ },
970
+
971
+ promptRequestSignal(this: OpencodeRuntime) {
972
+ return AbortSignal.timeout(this.currentPromptTimeoutMs());
973
+ },
974
+
975
+ healthProbeSignal(this: OpencodeRuntime, timeoutMs: number) {
976
+ return AbortSignal.timeout(timeoutMs);
977
+ },
978
+
979
+ healthProbeTimeoutMs(this: OpencodeRuntime) {
980
+ return Math.max(
981
+ 1_000,
982
+ Math.min(
983
+ this.currentPromptTimeoutMs(),
984
+ this.currentTimeoutMs(),
985
+ RUNTIME_HEALTH_TIMEOUT_CAP_MS,
986
+ ),
987
+ );
988
+ },
989
+
990
+ async waitFor(this: OpencodeRuntime, ms) {
991
+ if (this["disposed"]) return;
992
+ await new Promise<void>((resolve) => {
993
+ const timer = setTimeout(() => {
994
+ this["disposeController"].signal.removeEventListener("abort", onAbort);
995
+ resolve();
996
+ }, ms);
997
+ const onAbort = () => {
998
+ clearTimeout(timer);
999
+ this["disposeController"].signal.removeEventListener("abort", onAbort);
1000
+ resolve();
1001
+ };
1002
+ this["disposeController"].signal.addEventListener("abort", onAbort, {
1003
+ once: true,
1004
+ });
1005
+ });
1006
+ },
1007
+
1008
+ clearAllTimers(
1009
+ this: OpencodeRuntime,
1010
+ timers: Map<string, ReturnType<typeof setTimeout>>,
1011
+ ) {
1012
+ for (const timer of timers.values()) clearTimeout(timer);
1013
+ timers.clear();
1014
+ },
1015
+
1016
+ normalizeHealthProbeError(this: OpencodeRuntime, error, timeoutMs) {
1017
+ if (error instanceof DOMException && error.name === "AbortError") {
1018
+ return new Error(`Runtime health probe timed out after ${timeoutMs}ms.`);
1019
+ }
1020
+ return this.normalizeRuntimeError(error);
1021
+ },
1022
+
1023
+ async runHealthProbe(this: OpencodeRuntime) {
1024
+ const startedAt = Date.now();
1025
+ const timeoutMs = this.healthProbeTimeoutMs();
1026
+ const model: ResolvedModel = {
1027
+ providerId: this.currentProviderId(),
1028
+ modelId: this.currentModelId(),
1029
+ };
1030
+ let probeSessionId: string | null = null;
1031
+ let responseText: string | null = null;
1032
+ let normalizedError: Error | null = null;
1033
+ try {
1034
+ const created = unwrapSdkData<Session>(
1035
+ await this.getClient().session.create({
1036
+ body: { title: "agent-mockingbird-runtime-health" },
1037
+ responseStyle: "data",
1038
+ throwOnError: true,
1039
+ signal: this.healthProbeSignal(timeoutMs),
1040
+ }),
1041
+ );
1042
+ probeSessionId = created.id;
1043
+ const response = unwrapSdkData<{ info: Message; parts: Array<Part> }>(
1044
+ await this.getClient().session.prompt({
1045
+ path: { id: probeSessionId },
1046
+ body: {
1047
+ model: { providerID: model.providerId, modelID: model.modelId },
1048
+ parts: [{ type: "text", text: RUNTIME_HEALTH_PROMPT }],
1049
+ },
1050
+ responseStyle: "data",
1051
+ throwOnError: true,
1052
+ signal: this.healthProbeSignal(timeoutMs),
1053
+ }),
1054
+ );
1055
+ if (response.info.role !== "assistant") {
1056
+ throw new Error(
1057
+ `OpenCode returned unexpected message role: ${response.info.role}`,
1058
+ );
1059
+ }
1060
+ responseText = this.extractText(response.parts);
1061
+ if (!responseText) {
1062
+ throw new Error("Runtime health probe returned no assistant text.");
1063
+ }
1064
+ if (!RUNTIME_HEALTH_OK_PATTERN.test(responseText)) {
1065
+ throw new Error(
1066
+ `Runtime health probe response did not match expected pattern: ${RUNTIME_HEALTH_OK_PATTERN.source}`,
1067
+ );
1068
+ }
1069
+ } catch (error) {
1070
+ normalizedError = this.normalizeHealthProbeError(error, timeoutMs);
1071
+ }
1072
+ const checkedAtMs = Date.now();
1073
+ const cacheExpiresAtMs = checkedAtMs + RUNTIME_HEALTH_CACHE_TTL_MS;
1074
+ return {
1075
+ ok: normalizedError === null,
1076
+ checkedAt: new Date(checkedAtMs).toISOString(),
1077
+ latencyMs: checkedAtMs - startedAt,
1078
+ cacheTtlMs: RUNTIME_HEALTH_CACHE_TTL_MS,
1079
+ cacheExpiresAt: new Date(cacheExpiresAtMs).toISOString(),
1080
+ probeSessionId,
1081
+ responseText,
1082
+ error: normalizedError
1083
+ ? { name: normalizedError.name, message: normalizedError.message }
1084
+ : null,
1085
+ };
1086
+ },
1087
+
1088
+ runtimeConfigTargetKey(this: OpencodeRuntime) {
1089
+ return JSON.stringify({
1090
+ enabledSkills: this.currentEnabledSkills(),
1091
+ managedSkillsRoot: getManagedSkillsRootPath(
1092
+ this.currentRuntimeConfig()?.directory ?? null,
1093
+ ),
1094
+ });
1095
+ },
1096
+
1097
+ async ensureRuntimeConfigSynced(this: OpencodeRuntime, force = false) {
1098
+ if (this["options"].enableSmallModelSync === false) return;
1099
+ const targetKey = this.runtimeConfigTargetKey();
1100
+ if (!force && this["runtimeConfigSyncKey"] === targetKey) return;
1101
+ if (this["runtimeConfigSyncInFlight"]) {
1102
+ await this["runtimeConfigSyncInFlight"];
1103
+ if (!force && this["runtimeConfigSyncKey"] === targetKey) return;
1104
+ }
1105
+ const syncPromise = this.applyRuntimeConfigSync(targetKey);
1106
+ this["runtimeConfigSyncInFlight"] = syncPromise;
1107
+ try {
1108
+ await syncPromise;
1109
+ } finally {
1110
+ if (this["runtimeConfigSyncInFlight"] === syncPromise) {
1111
+ this["runtimeConfigSyncInFlight"] = null;
1112
+ }
1113
+ }
1114
+ },
1115
+
1116
+ async applyRuntimeConfigSync(this: OpencodeRuntime, targetKey) {
1117
+ try {
1118
+ const current = unwrapSdkData<Config>(
1119
+ await this.getClient().config.get({
1120
+ responseStyle: "data",
1121
+ throwOnError: true,
1122
+ signal: this.defaultRequestSignal(),
1123
+ }),
1124
+ );
1125
+ const nextConfig: Config = { ...current };
1126
+ const currentRecord = current as Record<string, unknown>;
1127
+ let changed = false;
1128
+ const desiredSkillPaths = normalizeStringArray(
1129
+ buildManagedSkillPaths(
1130
+ current,
1131
+ this.currentRuntimeConfig()?.directory ?? null,
1132
+ ),
1133
+ );
1134
+ const currentSkillsValue = currentRecord.skills;
1135
+ const currentSkillPaths = normalizeStringArray(
1136
+ isPlainObject(currentSkillsValue)
1137
+ ? (currentSkillsValue as { paths?: unknown }).paths
1138
+ : undefined,
1139
+ );
1140
+ if (!shallowEqualStringArrays(currentSkillPaths, desiredSkillPaths)) {
1141
+ const currentSkills = isPlainObject(currentSkillsValue)
1142
+ ? currentSkillsValue
1143
+ : {};
1144
+ (nextConfig as Record<string, unknown>).skills = {
1145
+ ...currentSkills,
1146
+ paths: desiredSkillPaths,
1147
+ };
1148
+ changed = true;
1149
+ }
1150
+ if (changed) {
1151
+ await this.getClient().config.update({
1152
+ body: nextConfig,
1153
+ responseStyle: "data",
1154
+ throwOnError: true,
1155
+ signal: this.defaultRequestSignal(),
1156
+ });
1157
+ }
1158
+ this["runtimeConfigSyncKey"] = targetKey;
1159
+ this["availableAgentNamesCache"] = null;
1160
+ } catch (error) {
1161
+ console.error(
1162
+ "[opencode] Config sync failed:",
1163
+ error instanceof Error ? error.message : error,
1164
+ );
1165
+ }
1166
+ },
1167
+ };