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,660 @@
1
+ import { CronExecutor } from "./executor";
2
+ import {
3
+ definitionRowToModel,
4
+ instanceRowToModel,
5
+ selectAll,
6
+ selectOne,
7
+ stepRowToModel,
8
+ } from "./repository";
9
+ import type { CronDefinitionRow, CronInstanceRow, CronStepRow } from "./repository";
10
+ import { ensureCronTables } from "./storage";
11
+ import type {
12
+ CronHealthSnapshot,
13
+ CronJobCreateInput,
14
+ CronJobDefinition,
15
+ CronJobInstance,
16
+ CronJobPatchInput,
17
+ CronJobState,
18
+ CronJobStep,
19
+ } from "./types";
20
+ import {
21
+ buildNormalizedJobInput,
22
+ computeDueTimesForDefinition,
23
+ createUniqueId,
24
+ nowMs,
25
+ normalizePayload,
26
+ validateMode,
27
+ validateSchedule,
28
+ } from "./utils";
29
+ import type { RuntimeEngine } from "../contracts/runtime";
30
+ import { sqlite } from "../db/client";
31
+ import { env } from "../env";
32
+ import { resolveRuntimeSessionScope } from "../runtime/sessionScope";
33
+
34
+ export class CronService {
35
+ private schedulerTimer: Timer | null = null;
36
+ private workerTimer: Timer | null = null;
37
+ private schedulerBusy = false;
38
+ private workerBusy = false;
39
+ private readonly workerId = `agent-mockingbird-${process.pid}`;
40
+ private readonly executor: CronExecutor;
41
+
42
+ constructor(private runtime: RuntimeEngine) {
43
+ ensureCronTables();
44
+ this.executor = new CronExecutor(runtime, {
45
+ getJob: (jobId) => this.getJob(jobId),
46
+ setInstanceState: (input) => this.setInstanceState(input),
47
+ });
48
+ }
49
+
50
+ start() {
51
+ if (!env.AGENT_MOCKINGBIRD_CRON_ENABLED) return;
52
+ this.schedulerTimer = setInterval(() => {
53
+ void this.schedulerTick();
54
+ }, env.AGENT_MOCKINGBIRD_CRON_SCHEDULER_POLL_MS);
55
+ this.workerTimer = setInterval(() => {
56
+ void this.workerTick();
57
+ }, env.AGENT_MOCKINGBIRD_CRON_WORKER_POLL_MS);
58
+ void this.schedulerTick();
59
+ void this.workerTick();
60
+ }
61
+
62
+ stop() {
63
+ if (this.schedulerTimer) clearInterval(this.schedulerTimer);
64
+ if (this.workerTimer) clearInterval(this.workerTimer);
65
+ this.schedulerTimer = null;
66
+ this.workerTimer = null;
67
+ }
68
+
69
+ async listJobs(): Promise<CronJobDefinition[]> {
70
+ ensureCronTables();
71
+ return selectAll<CronDefinitionRow>(
72
+ `
73
+ SELECT *
74
+ FROM cron_job_definitions
75
+ ORDER BY created_at DESC
76
+ `,
77
+ ).map(definitionRowToModel);
78
+ }
79
+
80
+ async getJob(jobId: string): Promise<CronJobDefinition | null> {
81
+ ensureCronTables();
82
+ const row = selectOne<CronDefinitionRow>(
83
+ `
84
+ SELECT *
85
+ FROM cron_job_definitions
86
+ WHERE id = ?1
87
+ `,
88
+ jobId,
89
+ );
90
+ return row ? definitionRowToModel(row) : null;
91
+ }
92
+
93
+ async createJob(input: CronJobCreateInput): Promise<CronJobDefinition> {
94
+ ensureCronTables();
95
+ const now = nowMs();
96
+ const normalized = buildNormalizedJobInput(input);
97
+ if (!normalized.name) throw new Error("name is required");
98
+ if (!normalized.id) throw new Error("id is required");
99
+
100
+ validateSchedule(normalized);
101
+ validateMode(normalized);
102
+
103
+ sqlite
104
+ .query(
105
+ `
106
+ INSERT INTO cron_job_definitions (
107
+ id, name, thread_session_id, enabled, schedule_kind, schedule_expr, every_ms, at_iso, timezone,
108
+ run_mode, handler_key, condition_module_path, condition_description, agent_prompt_template, agent_model_override,
109
+ max_attempts, retry_backoff_ms, payload_json, created_at, updated_at
110
+ )
111
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?19)
112
+ `,
113
+ )
114
+ .run(
115
+ normalized.id,
116
+ normalized.name,
117
+ null,
118
+ normalized.enabled ? 1 : 0,
119
+ normalized.scheduleKind,
120
+ normalized.scheduleExpr,
121
+ normalized.everyMs,
122
+ normalized.atIso,
123
+ normalized.timezone,
124
+ normalized.runMode,
125
+ null,
126
+ normalized.conditionModulePath,
127
+ normalized.conditionDescription,
128
+ normalized.agentPromptTemplate,
129
+ normalized.agentModelOverride,
130
+ normalized.maxAttempts,
131
+ normalized.retryBackoffMs,
132
+ JSON.stringify(normalizePayload(normalized.payload)),
133
+ now,
134
+ );
135
+
136
+ const created = await this.getJob(normalized.id);
137
+ if (!created) throw new Error("Failed to create cron job");
138
+ return created;
139
+ }
140
+
141
+ async upsertJob(input: CronJobCreateInput): Promise<{ created: boolean; job: CronJobDefinition }> {
142
+ const explicitId = input.id?.trim();
143
+ if (!explicitId) {
144
+ throw new Error("upsertJob requires job.id");
145
+ }
146
+ const existing = await this.getJob(explicitId);
147
+ if (!existing) {
148
+ return {
149
+ created: true,
150
+ job: await this.createJob({ ...input, id: explicitId }),
151
+ };
152
+ }
153
+
154
+ return {
155
+ created: false,
156
+ job: await this.updateJob(explicitId, input),
157
+ };
158
+ }
159
+
160
+ async updateJob(jobId: string, patch: CronJobPatchInput): Promise<CronJobDefinition> {
161
+ ensureCronTables();
162
+ const existing = await this.getJob(jobId);
163
+ if (!existing) throw new Error(`Unknown cron job: ${jobId}`);
164
+
165
+ const merged = buildNormalizedJobInput({ ...patch, id: jobId }, existing);
166
+ validateSchedule(merged);
167
+ validateMode(merged);
168
+
169
+ sqlite
170
+ .query(
171
+ `
172
+ UPDATE cron_job_definitions
173
+ SET
174
+ name = ?2,
175
+ thread_session_id = ?3,
176
+ enabled = ?4,
177
+ schedule_kind = ?5,
178
+ schedule_expr = ?6,
179
+ every_ms = ?7,
180
+ at_iso = ?8,
181
+ timezone = ?9,
182
+ run_mode = ?10,
183
+ handler_key = ?11,
184
+ condition_module_path = ?12,
185
+ condition_description = ?13,
186
+ agent_prompt_template = ?14,
187
+ agent_model_override = ?15,
188
+ max_attempts = ?16,
189
+ retry_backoff_ms = ?17,
190
+ payload_json = ?18,
191
+ updated_at = ?19
192
+ WHERE id = ?1
193
+ `,
194
+ )
195
+ .run(
196
+ jobId,
197
+ merged.name,
198
+ existing.threadSessionId,
199
+ merged.enabled ? 1 : 0,
200
+ merged.scheduleKind,
201
+ merged.scheduleExpr,
202
+ merged.everyMs,
203
+ merged.atIso,
204
+ merged.timezone,
205
+ merged.runMode,
206
+ null,
207
+ merged.conditionModulePath,
208
+ merged.conditionDescription,
209
+ merged.agentPromptTemplate,
210
+ merged.agentModelOverride,
211
+ merged.maxAttempts,
212
+ merged.retryBackoffMs,
213
+ JSON.stringify(merged.payload),
214
+ nowMs(),
215
+ );
216
+
217
+ const updated = await this.getJob(jobId);
218
+ if (!updated) throw new Error(`Unknown cron job: ${jobId}`);
219
+ return updated;
220
+ }
221
+
222
+ async deleteJob(jobId: string): Promise<{ removed: boolean }> {
223
+ ensureCronTables();
224
+ const changes = sqlite
225
+ .query("DELETE FROM cron_job_definitions WHERE id = ?1")
226
+ .run(jobId).changes;
227
+ return { removed: changes > 0 };
228
+ }
229
+
230
+ async listInstances(input?: { jobId?: string; limit?: number }): Promise<CronJobInstance[]> {
231
+ ensureCronTables();
232
+ const limit = Math.max(1, Math.min(500, input?.limit ?? 100));
233
+ const rows = input?.jobId
234
+ ? selectAll<CronInstanceRow>(
235
+ `
236
+ SELECT
237
+ i.*,
238
+ EXISTS(
239
+ SELECT 1
240
+ FROM cron_job_steps s
241
+ WHERE s.job_instance_id = i.id
242
+ AND s.step_kind = 'agent'
243
+ ) AS agent_invoked
244
+ FROM cron_job_instances i
245
+ WHERE i.job_definition_id = ?1
246
+ ORDER BY i.created_at DESC
247
+ LIMIT ?2
248
+ `,
249
+ input.jobId,
250
+ limit,
251
+ )
252
+ : selectAll<CronInstanceRow>(
253
+ `
254
+ SELECT
255
+ i.*,
256
+ EXISTS(
257
+ SELECT 1
258
+ FROM cron_job_steps s
259
+ WHERE s.job_instance_id = i.id
260
+ AND s.step_kind = 'agent'
261
+ ) AS agent_invoked
262
+ FROM cron_job_instances i
263
+ ORDER BY i.created_at DESC
264
+ LIMIT ?1
265
+ `,
266
+ limit,
267
+ );
268
+ return rows.map(instanceRowToModel);
269
+ }
270
+
271
+ async listSteps(instanceId: string): Promise<CronJobStep[]> {
272
+ ensureCronTables();
273
+ return selectAll<CronStepRow>(
274
+ `
275
+ SELECT *
276
+ FROM cron_job_steps
277
+ WHERE job_instance_id = ?1
278
+ ORDER BY created_at ASC
279
+ `,
280
+ instanceId,
281
+ ).map(stepRowToModel);
282
+ }
283
+
284
+ async runJobNow(jobId: string): Promise<{ queued: boolean; instanceId: string | null }> {
285
+ ensureCronTables();
286
+ const definition = await this.getJob(jobId);
287
+ if (!definition) throw new Error(`Unknown cron job: ${jobId}`);
288
+
289
+ const baseScheduled = nowMs();
290
+ for (let offset = 0; offset < 10; offset += 1) {
291
+ const scheduledFor = baseScheduled + offset;
292
+ const instanceId = createUniqueId("ins");
293
+ const inserted = sqlite
294
+ .query(
295
+ `
296
+ INSERT INTO cron_job_instances (
297
+ id, job_definition_id, scheduled_for, state, attempt, next_attempt_at,
298
+ lease_owner, lease_expires_at, last_heartbeat_at,
299
+ result_summary, error_json, created_at, updated_at
300
+ )
301
+ VALUES (?1, ?2, ?3, 'queued', 0, NULL, NULL, NULL, NULL, NULL, NULL, ?4, ?4)
302
+ ON CONFLICT(job_definition_id, scheduled_for) DO NOTHING
303
+ `,
304
+ )
305
+ .run(instanceId, jobId, scheduledFor, baseScheduled);
306
+ if (inserted.changes > 0) {
307
+ return { queued: true, instanceId };
308
+ }
309
+ }
310
+ return { queued: false, instanceId: null };
311
+ }
312
+
313
+ async getHealth(): Promise<CronHealthSnapshot> {
314
+ ensureCronTables();
315
+ const jobs =
316
+ selectOne<{ total: number; enabled: number }>(
317
+ `
318
+ SELECT
319
+ COUNT(*) AS total,
320
+ COALESCE(SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END), 0) AS enabled
321
+ FROM cron_job_definitions
322
+ `,
323
+ ) ?? { total: 0, enabled: 0 };
324
+
325
+ const instances =
326
+ selectOne<{
327
+ queued: number;
328
+ leased: number;
329
+ running: number;
330
+ completed: number;
331
+ failed: number;
332
+ dead: number;
333
+ }>(
334
+ `
335
+ SELECT
336
+ COALESCE(SUM(CASE WHEN state = 'queued' THEN 1 ELSE 0 END), 0) AS queued,
337
+ COALESCE(SUM(CASE WHEN state = 'leased' THEN 1 ELSE 0 END), 0) AS leased,
338
+ COALESCE(SUM(CASE WHEN state = 'running' THEN 1 ELSE 0 END), 0) AS running,
339
+ COALESCE(SUM(CASE WHEN state = 'completed' THEN 1 ELSE 0 END), 0) AS completed,
340
+ COALESCE(SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END), 0) AS failed,
341
+ COALESCE(SUM(CASE WHEN state = 'dead' THEN 1 ELSE 0 END), 0) AS dead
342
+ FROM cron_job_instances
343
+ `,
344
+ ) ?? {
345
+ queued: 0,
346
+ leased: 0,
347
+ running: 0,
348
+ completed: 0,
349
+ failed: 0,
350
+ dead: 0,
351
+ };
352
+
353
+ return {
354
+ enabled: env.AGENT_MOCKINGBIRD_CRON_ENABLED,
355
+ schedulerPollMs: env.AGENT_MOCKINGBIRD_CRON_SCHEDULER_POLL_MS,
356
+ workerPollMs: env.AGENT_MOCKINGBIRD_CRON_WORKER_POLL_MS,
357
+ leaseMs: env.AGENT_MOCKINGBIRD_CRON_LEASE_MS,
358
+ jobs,
359
+ instances,
360
+ };
361
+ }
362
+
363
+ describeContract() {
364
+ return {
365
+ runModes: {
366
+ background: {
367
+ requires: ["conditionModulePath"],
368
+ optional: ["conditionDescription"],
369
+ forbids: ["agentPromptTemplate"],
370
+ moduleContract: {
371
+ contextKeys: ["nowMs", "payload", "job", "instance"],
372
+ resultShape: "CronHandlerResult",
373
+ forbids: ["invokeAgent"],
374
+ },
375
+ },
376
+ agent: {
377
+ requires: ["agentPromptTemplate"],
378
+ forbids: ["conditionModulePath", "conditionDescription"],
379
+ },
380
+ conditional_agent: {
381
+ requires: ["conditionModulePath"],
382
+ optional: ["conditionDescription", "agentPromptTemplate"],
383
+ moduleContract: {
384
+ contextKeys: ["nowMs", "payload", "job", "instance"],
385
+ resultShape: "CronHandlerResult",
386
+ },
387
+ },
388
+ },
389
+ };
390
+ }
391
+
392
+ getJobByThreadSessionId(sessionId: string): CronJobDefinition | null {
393
+ const normalizedSessionId = sessionId.trim();
394
+ if (!normalizedSessionId) return null;
395
+ const row = selectOne<CronDefinitionRow>(
396
+ `
397
+ SELECT *
398
+ FROM cron_job_definitions
399
+ WHERE thread_session_id = ?1
400
+ LIMIT 1
401
+ `,
402
+ normalizedSessionId,
403
+ );
404
+ return row ? definitionRowToModel(row) : null;
405
+ }
406
+
407
+ async notifyMainThread(input: {
408
+ runtimeSessionId: string;
409
+ prompt: string;
410
+ severity?: "info" | "warn" | "critical";
411
+ }): Promise<{
412
+ delivered: true;
413
+ threadSessionId: string;
414
+ sourceKind: "cron" | "heartbeat";
415
+ cronJobId?: string;
416
+ }> {
417
+ const runtimeSessionId = input.runtimeSessionId.trim();
418
+ const prompt = input.prompt.trim();
419
+ if (!runtimeSessionId) throw new Error("runtimeSessionId is required");
420
+ if (!prompt) throw new Error("prompt is required");
421
+
422
+ const scope = resolveRuntimeSessionScope(runtimeSessionId, this);
423
+ const threadSessionId = scope.localSessionId;
424
+ if (!threadSessionId) throw new Error("Unknown runtime session");
425
+ if (!scope.mayNotifyMain) {
426
+ throw new Error("notify_main_thread is only available from cron or heartbeat threads");
427
+ }
428
+
429
+ const severity = input.severity ?? "info";
430
+ const heading =
431
+ scope.kind === "cron"
432
+ ? `Cron escalation from ${scope.cronJobName ?? scope.cronJobId ?? "cron"} (${scope.cronJobId ?? "cron"})`
433
+ : "Heartbeat escalation";
434
+ await this.runtime.sendUserMessage({
435
+ sessionId: "main",
436
+ content: [heading, `Severity: ${severity}`, "", prompt].join("\n"),
437
+ metadata: {
438
+ source: scope.kind,
439
+ cronJobId: scope.cronJobId,
440
+ cronThreadSessionId: scope.kind === "cron" ? threadSessionId : undefined,
441
+ heartbeatThreadSessionId: scope.kind === "heartbeat" ? threadSessionId : undefined,
442
+ severity,
443
+ },
444
+ });
445
+
446
+ const result: {
447
+ delivered: true,
448
+ threadSessionId: string;
449
+ sourceKind: "cron" | "heartbeat";
450
+ cronJobId?: string;
451
+ } = {
452
+ delivered: true,
453
+ threadSessionId,
454
+ sourceKind: scope.kind === "heartbeat" ? "heartbeat" : "cron",
455
+ };
456
+ if (scope.cronJobId) {
457
+ result.cronJobId = scope.cronJobId;
458
+ }
459
+ return result;
460
+ }
461
+
462
+ private async schedulerTick() {
463
+ if (!env.AGENT_MOCKINGBIRD_CRON_ENABLED || this.schedulerBusy) return;
464
+ this.schedulerBusy = true;
465
+ try {
466
+ ensureCronTables();
467
+ const now = nowMs();
468
+ const definitions = selectAll<CronDefinitionRow>(
469
+ `
470
+ SELECT *
471
+ FROM cron_job_definitions
472
+ WHERE enabled = 1
473
+ `,
474
+ );
475
+
476
+ for (const row of definitions) {
477
+ const dueTimes = computeDueTimesForDefinition(
478
+ row,
479
+ now,
480
+ env.AGENT_MOCKINGBIRD_CRON_MAX_ENQUEUE_PER_JOB_TICK,
481
+ );
482
+ if (!dueTimes.length) continue;
483
+ let lastEnqueued = row.last_enqueued_for ?? null;
484
+ for (const scheduledFor of dueTimes) {
485
+ sqlite
486
+ .query(
487
+ `
488
+ INSERT INTO cron_job_instances (
489
+ id, job_definition_id, scheduled_for, state, attempt, next_attempt_at,
490
+ lease_owner, lease_expires_at, last_heartbeat_at,
491
+ result_summary, error_json, created_at, updated_at
492
+ )
493
+ VALUES (?1, ?2, ?3, 'queued', 0, NULL, NULL, NULL, NULL, NULL, NULL, ?4, ?4)
494
+ ON CONFLICT(job_definition_id, scheduled_for) DO NOTHING
495
+ `,
496
+ )
497
+ .run(createUniqueId("ins"), row.id, scheduledFor, now);
498
+ if (lastEnqueued === null || scheduledFor > lastEnqueued) {
499
+ lastEnqueued = scheduledFor;
500
+ }
501
+ }
502
+ if (lastEnqueued !== null && lastEnqueued !== row.last_enqueued_for) {
503
+ sqlite
504
+ .query(
505
+ `
506
+ UPDATE cron_job_definitions
507
+ SET last_enqueued_for = ?2, updated_at = ?3
508
+ WHERE id = ?1
509
+ `,
510
+ )
511
+ .run(row.id, lastEnqueued, now);
512
+ }
513
+ }
514
+ } finally {
515
+ this.schedulerBusy = false;
516
+ }
517
+ }
518
+
519
+ private reclaimExpiredLeases(now: number) {
520
+ sqlite
521
+ .query(
522
+ `
523
+ UPDATE cron_job_instances
524
+ SET
525
+ state = 'queued',
526
+ lease_owner = NULL,
527
+ lease_expires_at = NULL,
528
+ last_heartbeat_at = NULL,
529
+ updated_at = ?2
530
+ WHERE id IN (
531
+ SELECT id
532
+ FROM cron_job_instances
533
+ WHERE state IN ('leased', 'running')
534
+ AND lease_expires_at IS NOT NULL
535
+ AND lease_expires_at <= ?1
536
+ )
537
+ `,
538
+ )
539
+ .run(now, now);
540
+ }
541
+
542
+ private claimNextInstance(now: number): CronInstanceRow | null {
543
+ const tx = sqlite.transaction(() => {
544
+ this.reclaimExpiredLeases(now);
545
+ const candidate = selectOne<CronInstanceRow>(
546
+ `
547
+ SELECT *
548
+ FROM cron_job_instances
549
+ WHERE (state = 'queued' OR state = 'failed')
550
+ AND (next_attempt_at IS NULL OR next_attempt_at <= ?1)
551
+ ORDER BY scheduled_for ASC
552
+ LIMIT 1
553
+ `,
554
+ now,
555
+ );
556
+ if (!candidate) return null;
557
+
558
+ const claimed = sqlite
559
+ .query(
560
+ `
561
+ UPDATE cron_job_instances
562
+ SET
563
+ state = 'leased',
564
+ lease_owner = ?2,
565
+ lease_expires_at = ?3,
566
+ last_heartbeat_at = ?1,
567
+ updated_at = ?1
568
+ WHERE id = ?4
569
+ AND (state = 'queued' OR state = 'failed')
570
+ `,
571
+ )
572
+ .run(now, this.workerId, now + env.AGENT_MOCKINGBIRD_CRON_LEASE_MS, candidate.id);
573
+ if (claimed.changes < 1) return null;
574
+
575
+ return selectOne<CronInstanceRow>(
576
+ `
577
+ SELECT *
578
+ FROM cron_job_instances
579
+ WHERE id = ?1
580
+ `,
581
+ candidate.id,
582
+ );
583
+ });
584
+ return tx();
585
+ }
586
+
587
+ private async workerTick() {
588
+ if (!env.AGENT_MOCKINGBIRD_CRON_ENABLED || this.workerBusy) return;
589
+ this.workerBusy = true;
590
+ try {
591
+ const claimed = this.claimNextInstance(nowMs());
592
+ if (!claimed) return;
593
+
594
+ const definitionRow = this.loadDefinitionById(claimed.job_definition_id);
595
+ if (!definitionRow) {
596
+ this.setInstanceState({
597
+ instanceId: claimed.id,
598
+ state: "dead",
599
+ error: { message: `missing job definition ${claimed.job_definition_id}` },
600
+ });
601
+ return;
602
+ }
603
+
604
+ await this.executor.executeInstance(
605
+ claimed,
606
+ definitionRowToModel(definitionRow),
607
+ instanceRowToModel(claimed),
608
+ );
609
+ } finally {
610
+ this.workerBusy = false;
611
+ }
612
+ }
613
+
614
+ private loadDefinitionById(jobId: string): CronDefinitionRow | null {
615
+ return selectOne<CronDefinitionRow>(
616
+ `
617
+ SELECT *
618
+ FROM cron_job_definitions
619
+ WHERE id = ?1
620
+ `,
621
+ jobId,
622
+ );
623
+ }
624
+
625
+ private setInstanceState(input: {
626
+ instanceId: string;
627
+ state: CronJobState;
628
+ attempt?: number;
629
+ nextAttemptAt?: number | null;
630
+ resultSummary?: string | null;
631
+ error?: unknown;
632
+ }) {
633
+ sqlite
634
+ .query(
635
+ `
636
+ UPDATE cron_job_instances
637
+ SET
638
+ state = ?2,
639
+ attempt = COALESCE(?3, attempt),
640
+ next_attempt_at = ?4,
641
+ lease_owner = NULL,
642
+ lease_expires_at = NULL,
643
+ last_heartbeat_at = NULL,
644
+ result_summary = ?5,
645
+ error_json = ?6,
646
+ updated_at = ?7
647
+ WHERE id = ?1
648
+ `,
649
+ )
650
+ .run(
651
+ input.instanceId,
652
+ input.state,
653
+ input.attempt ?? null,
654
+ input.nextAttemptAt ?? null,
655
+ input.resultSummary ?? null,
656
+ input.error === undefined ? null : JSON.stringify(input.error),
657
+ nowMs(),
658
+ );
659
+ }
660
+ }