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,92 @@
1
+ import { sqlite } from "../db/client";
2
+
3
+ function tableHasColumn(tableName: string, columnName: string): boolean {
4
+ const columns = sqlite.query(`PRAGMA table_info(${tableName})`).all() as Array<{ name?: unknown }>;
5
+ return columns.some(column => column.name === columnName);
6
+ }
7
+
8
+ export function ensureCronTables() {
9
+ sqlite.exec(`
10
+ CREATE TABLE IF NOT EXISTS cron_job_definitions (
11
+ id TEXT PRIMARY KEY,
12
+ name TEXT NOT NULL,
13
+ thread_session_id TEXT,
14
+ enabled INTEGER NOT NULL DEFAULT 1,
15
+ schedule_kind TEXT NOT NULL CHECK (schedule_kind IN ('at', 'every', 'cron')),
16
+ schedule_expr TEXT,
17
+ every_ms INTEGER,
18
+ at_iso TEXT,
19
+ timezone TEXT,
20
+ run_mode TEXT NOT NULL CHECK (run_mode IN ('background', 'conditional_agent', 'agent')),
21
+ handler_key TEXT,
22
+ condition_module_path TEXT,
23
+ condition_description TEXT,
24
+ agent_prompt_template TEXT,
25
+ agent_model_override TEXT,
26
+ max_attempts INTEGER NOT NULL DEFAULT 3,
27
+ retry_backoff_ms INTEGER NOT NULL DEFAULT 30000,
28
+ payload_json TEXT NOT NULL DEFAULT '{}',
29
+ last_enqueued_for INTEGER,
30
+ created_at INTEGER NOT NULL,
31
+ updated_at INTEGER NOT NULL
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS cron_job_definitions_enabled_idx
35
+ ON cron_job_definitions(enabled, schedule_kind);
36
+
37
+ CREATE TABLE IF NOT EXISTS cron_job_instances (
38
+ id TEXT PRIMARY KEY,
39
+ job_definition_id TEXT NOT NULL REFERENCES cron_job_definitions(id) ON DELETE CASCADE,
40
+ scheduled_for INTEGER NOT NULL,
41
+ state TEXT NOT NULL CHECK (state IN ('queued', 'leased', 'running', 'completed', 'failed', 'dead')),
42
+ attempt INTEGER NOT NULL DEFAULT 0,
43
+ next_attempt_at INTEGER,
44
+ lease_owner TEXT,
45
+ lease_expires_at INTEGER,
46
+ last_heartbeat_at INTEGER,
47
+ result_summary TEXT,
48
+ error_json TEXT,
49
+ created_at INTEGER NOT NULL,
50
+ updated_at INTEGER NOT NULL,
51
+ UNIQUE(job_definition_id, scheduled_for)
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS cron_job_instances_ready_idx
55
+ ON cron_job_instances(state, next_attempt_at, scheduled_for);
56
+ CREATE INDEX IF NOT EXISTS cron_job_instances_job_idx
57
+ ON cron_job_instances(job_definition_id, created_at DESC);
58
+
59
+ CREATE TABLE IF NOT EXISTS cron_job_steps (
60
+ id TEXT PRIMARY KEY,
61
+ job_instance_id TEXT NOT NULL REFERENCES cron_job_instances(id) ON DELETE CASCADE,
62
+ step_kind TEXT NOT NULL CHECK (step_kind IN ('background', 'conditional_agent', 'agent')),
63
+ status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
64
+ input_json TEXT,
65
+ output_json TEXT,
66
+ error_json TEXT,
67
+ started_at INTEGER,
68
+ finished_at INTEGER,
69
+ created_at INTEGER NOT NULL
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS cron_job_steps_instance_idx
73
+ ON cron_job_steps(job_instance_id, created_at ASC);
74
+ `);
75
+
76
+ if (!tableHasColumn("cron_job_definitions", "condition_module_path")) {
77
+ sqlite.exec("ALTER TABLE cron_job_definitions ADD COLUMN condition_module_path TEXT");
78
+ }
79
+ if (!tableHasColumn("cron_job_definitions", "condition_description")) {
80
+ sqlite.exec("ALTER TABLE cron_job_definitions ADD COLUMN condition_description TEXT");
81
+ }
82
+ if (!tableHasColumn("cron_job_definitions", "thread_session_id")) {
83
+ sqlite.exec("ALTER TABLE cron_job_definitions ADD COLUMN thread_session_id TEXT");
84
+ }
85
+ }
86
+
87
+ export function clearCronTables() {
88
+ ensureCronTables();
89
+ sqlite.query("DELETE FROM cron_job_steps").run();
90
+ sqlite.query("DELETE FROM cron_job_instances").run();
91
+ sqlite.query("DELETE FROM cron_job_definitions").run();
92
+ }
@@ -0,0 +1,138 @@
1
+ export type CronScheduleKind = "at" | "every" | "cron";
2
+ export type CronRunMode = "background" | "conditional_agent" | "agent";
3
+
4
+ export type CronJobState = "queued" | "leased" | "running" | "completed" | "failed" | "dead";
5
+ export type CronStepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
6
+ export type CronStepKind = "background" | "conditional_agent" | "agent";
7
+
8
+ export interface CronJobDefinition {
9
+ id: string;
10
+ name: string;
11
+ threadSessionId: string | null;
12
+ enabled: boolean;
13
+ scheduleKind: CronScheduleKind;
14
+ scheduleExpr: string | null;
15
+ everyMs: number | null;
16
+ atIso: string | null;
17
+ timezone: string | null;
18
+ runMode: CronRunMode;
19
+ conditionModulePath: string | null;
20
+ conditionDescription: string | null;
21
+ agentPromptTemplate: string | null;
22
+ agentModelOverride: string | null;
23
+ maxAttempts: number;
24
+ retryBackoffMs: number;
25
+ payload: Record<string, unknown>;
26
+ lastEnqueuedFor: string | null;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+
31
+ export interface CronJobInstance {
32
+ id: string;
33
+ jobDefinitionId: string;
34
+ scheduledFor: string;
35
+ agentInvoked: boolean;
36
+ state: CronJobState;
37
+ attempt: number;
38
+ nextAttemptAt: string | null;
39
+ leaseOwner: string | null;
40
+ leaseExpiresAt: string | null;
41
+ lastHeartbeatAt: string | null;
42
+ resultSummary: string | null;
43
+ error: unknown;
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ }
47
+
48
+ export interface CronJobStep {
49
+ id: string;
50
+ jobInstanceId: string;
51
+ stepKind: CronStepKind;
52
+ status: CronStepStatus;
53
+ input: unknown;
54
+ output: unknown;
55
+ error: unknown;
56
+ startedAt: string | null;
57
+ finishedAt: string | null;
58
+ createdAt: string;
59
+ }
60
+
61
+ export interface CronHealthSnapshot {
62
+ enabled: boolean;
63
+ schedulerPollMs: number;
64
+ workerPollMs: number;
65
+ leaseMs: number;
66
+ jobs: {
67
+ total: number;
68
+ enabled: number;
69
+ };
70
+ instances: {
71
+ queued: number;
72
+ leased: number;
73
+ running: number;
74
+ completed: number;
75
+ failed: number;
76
+ dead: number;
77
+ };
78
+ }
79
+
80
+ export interface CronJobCreateInput {
81
+ id?: string;
82
+ name: string;
83
+ enabled?: boolean;
84
+ scheduleKind: CronScheduleKind;
85
+ scheduleExpr?: string | null;
86
+ everyMs?: number | null;
87
+ atIso?: string | null;
88
+ timezone?: string | null;
89
+ runMode: CronRunMode;
90
+ conditionModulePath?: string | null;
91
+ conditionDescription?: string | null;
92
+ agentPromptTemplate?: string | null;
93
+ agentModelOverride?: string | null;
94
+ maxAttempts?: number;
95
+ retryBackoffMs?: number;
96
+ payload?: Record<string, unknown>;
97
+ }
98
+
99
+ export interface CronJobPatchInput {
100
+ name?: string;
101
+ enabled?: boolean;
102
+ scheduleKind?: CronScheduleKind;
103
+ scheduleExpr?: string | null;
104
+ everyMs?: number | null;
105
+ atIso?: string | null;
106
+ timezone?: string | null;
107
+ runMode?: CronRunMode;
108
+ conditionModulePath?: string | null;
109
+ conditionDescription?: string | null;
110
+ agentPromptTemplate?: string | null;
111
+ agentModelOverride?: string | null;
112
+ maxAttempts?: number;
113
+ retryBackoffMs?: number;
114
+ payload?: Record<string, unknown>;
115
+ }
116
+
117
+ export interface CronHandlerResult {
118
+ status: "ok" | "error";
119
+ summary?: string;
120
+ data?: unknown;
121
+ invokeAgent?: {
122
+ shouldInvoke: boolean;
123
+ prompt?: string;
124
+ context?: Record<string, unknown>;
125
+ severity?: "info" | "warn" | "critical";
126
+ };
127
+ }
128
+
129
+ export interface CronConditionalModuleContext {
130
+ nowMs: number;
131
+ payload: Record<string, unknown>;
132
+ job: CronJobDefinition;
133
+ instance: CronJobInstance;
134
+ }
135
+
136
+ export type CronConditionalModule = (
137
+ ctx: CronConditionalModuleContext,
138
+ ) => Promise<CronHandlerResult> | CronHandlerResult;
@@ -0,0 +1,351 @@
1
+ import { CronTime, validateCronExpression } from "cron";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { extname, relative, resolve } from "node:path";
4
+
5
+ import type {
6
+ CronHandlerResult,
7
+ CronJobCreateInput,
8
+ CronJobDefinition,
9
+ CronJobInstance,
10
+ CronJobPatchInput,
11
+ CronRunMode,
12
+ CronScheduleKind,
13
+ } from "./types";
14
+ import { getConfigSnapshot } from "../config/service";
15
+ import { createLogger } from "../logging/logger";
16
+
17
+ const CONDITION_MODULE_EXTENSIONS = new Set([".ts", ".js", ".mjs", ".cjs"]);
18
+ const logger = createLogger("cron");
19
+
20
+ export function nowMs() {
21
+ return Date.now();
22
+ }
23
+
24
+ export function toIso(ms: number | null | undefined): string | null {
25
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
26
+ return new Date(ms).toISOString();
27
+ }
28
+
29
+ export function parseJson(value: string | null): unknown {
30
+ if (!value) return null;
31
+ try {
32
+ return JSON.parse(value);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function normalizePayload(
39
+ input: Record<string, unknown> | undefined,
40
+ ): Record<string, unknown> {
41
+ return input && typeof input === "object" && !Array.isArray(input)
42
+ ? input
43
+ : {};
44
+ }
45
+
46
+ export function normalizeConditionalModuleResult(
47
+ value: unknown,
48
+ ): CronHandlerResult {
49
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
50
+ throw new Error("conditional module must return an object");
51
+ }
52
+ const result = value as Record<string, unknown>;
53
+ if (result.status !== "ok" && result.status !== "error") {
54
+ throw new Error("conditional module result.status must be 'ok' or 'error'");
55
+ }
56
+ return value as CronHandlerResult;
57
+ }
58
+
59
+ function readLegacyHandlerKey(input: { handlerKey?: unknown }): string | null {
60
+ return typeof input.handlerKey === "string" ? input.handlerKey.trim() || null : null;
61
+ }
62
+
63
+ export function validateSchedule(input: {
64
+ scheduleKind: CronScheduleKind;
65
+ scheduleExpr: string | null;
66
+ everyMs: number | null;
67
+ atIso: string | null;
68
+ }) {
69
+ if (input.scheduleKind === "at") {
70
+ if (!input.atIso) throw new Error("scheduleKind=at requires atIso");
71
+ const parsedAt = Date.parse(input.atIso);
72
+ if (!Number.isFinite(parsedAt)) {
73
+ throw new Error("atIso must be a valid ISO timestamp");
74
+ }
75
+ return;
76
+ }
77
+
78
+ if (input.scheduleKind === "every") {
79
+ if (
80
+ typeof input.everyMs !== "number" ||
81
+ !Number.isFinite(input.everyMs) ||
82
+ input.everyMs < 1_000
83
+ ) {
84
+ throw new Error("scheduleKind=every requires everyMs >= 1000");
85
+ }
86
+ return;
87
+ }
88
+
89
+ const expr = input.scheduleExpr?.trim();
90
+ if (!expr) throw new Error("scheduleKind=cron requires scheduleExpr");
91
+ const valid = validateCronExpression(expr);
92
+ if (!valid.valid) {
93
+ throw new Error(`invalid cron expression: ${valid.error?.message ?? "parse failed"}`);
94
+ }
95
+ }
96
+
97
+ export function validateMode(input: {
98
+ runMode: CronRunMode;
99
+ conditionModulePath: string | null;
100
+ conditionDescription: string | null;
101
+ agentPromptTemplate: string | null;
102
+ handlerKey?: string | null;
103
+ }) {
104
+ if (input.handlerKey?.trim()) {
105
+ throw new Error("handlerKey is no longer supported");
106
+ }
107
+
108
+ if (input.runMode === "background") {
109
+ if (!input.conditionModulePath?.trim()) {
110
+ throw new Error("runMode=background requires conditionModulePath");
111
+ }
112
+ if (input.agentPromptTemplate?.trim()) {
113
+ throw new Error("runMode=background does not allow agentPromptTemplate");
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (input.runMode === "agent") {
119
+ if (input.conditionModulePath) {
120
+ throw new Error("runMode=agent does not allow conditionModulePath");
121
+ }
122
+ if (input.conditionDescription) {
123
+ throw new Error("runMode=agent does not allow conditionDescription");
124
+ }
125
+ if (!input.agentPromptTemplate?.trim()) {
126
+ throw new Error("runMode=agent requires agentPromptTemplate");
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (!input.conditionModulePath?.trim()) {
132
+ throw new Error("runMode=conditional_agent requires conditionModulePath");
133
+ }
134
+ }
135
+
136
+ export function computeBackoffMs(base: number, attempt: number): number {
137
+ const cappedAttempt = Math.max(1, Math.min(10, attempt));
138
+ return Math.min(
139
+ base * 2 ** (cappedAttempt - 1),
140
+ getConfigSnapshot().config.runtime.cron.retryBackoffCapMs,
141
+ );
142
+ }
143
+
144
+ export function renderTemplate(
145
+ template: string,
146
+ ctx: Record<string, unknown>,
147
+ ): string {
148
+ return template.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_full, key: string) => {
149
+ const raw = ctx[key];
150
+ if (raw === undefined || raw === null) return "";
151
+ if (typeof raw === "string") return raw;
152
+ if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
153
+ return JSON.stringify(raw);
154
+ });
155
+ }
156
+
157
+ export function createUniqueId(prefix: string): string {
158
+ return `${prefix}-${crypto.randomUUID().slice(0, 8)}`;
159
+ }
160
+
161
+ export function definitionPayloadContext(
162
+ definition: CronJobDefinition,
163
+ extra?: Record<string, unknown>,
164
+ ) {
165
+ return {
166
+ ...definition.payload,
167
+ jobId: definition.id,
168
+ jobName: definition.name,
169
+ ...(extra ?? {}),
170
+ };
171
+ }
172
+
173
+ export function buildNormalizedJobInput(
174
+ input: CronJobCreateInput | (CronJobPatchInput & { id?: string }),
175
+ defaults?: CronJobDefinition,
176
+ ): {
177
+ id: string;
178
+ name: string;
179
+ enabled: boolean;
180
+ scheduleKind: CronScheduleKind;
181
+ scheduleExpr: string | null;
182
+ everyMs: number | null;
183
+ atIso: string | null;
184
+ timezone: string | null;
185
+ runMode: CronRunMode;
186
+ handlerKey: string | null;
187
+ conditionModulePath: string | null;
188
+ conditionDescription: string | null;
189
+ agentPromptTemplate: string | null;
190
+ agentModelOverride: string | null;
191
+ maxAttempts: number;
192
+ retryBackoffMs: number;
193
+ payload: Record<string, unknown>;
194
+ } {
195
+ return {
196
+ id: input.id?.trim() ? input.id.trim() : defaults?.id ?? createUniqueId("cron"),
197
+ name: input.name?.trim() ?? defaults?.name ?? "",
198
+ enabled: input.enabled ?? defaults?.enabled ?? true,
199
+ scheduleKind: (input.scheduleKind ?? defaults?.scheduleKind)!,
200
+ scheduleExpr:
201
+ input.scheduleExpr !== undefined
202
+ ? input.scheduleExpr?.trim() ?? null
203
+ : defaults?.scheduleExpr ?? null,
204
+ everyMs: input.everyMs !== undefined ? input.everyMs : defaults?.everyMs ?? null,
205
+ atIso: input.atIso !== undefined ? input.atIso?.trim() ?? null : defaults?.atIso ?? null,
206
+ timezone:
207
+ input.timezone !== undefined ? input.timezone?.trim() ?? null : defaults?.timezone ?? null,
208
+ runMode: (input.runMode ?? defaults?.runMode)!,
209
+ handlerKey: readLegacyHandlerKey(input as { handlerKey?: unknown }),
210
+ conditionModulePath:
211
+ input.conditionModulePath !== undefined
212
+ ? input.conditionModulePath?.trim() ?? null
213
+ : defaults?.conditionModulePath ?? null,
214
+ conditionDescription:
215
+ input.conditionDescription !== undefined
216
+ ? input.conditionDescription?.trim() ?? null
217
+ : defaults?.conditionDescription ?? null,
218
+ agentPromptTemplate:
219
+ input.agentPromptTemplate !== undefined
220
+ ? input.agentPromptTemplate?.trim() ?? null
221
+ : defaults?.agentPromptTemplate ?? null,
222
+ agentModelOverride:
223
+ input.agentModelOverride !== undefined
224
+ ? input.agentModelOverride?.trim() ?? null
225
+ : defaults?.agentModelOverride ?? null,
226
+ maxAttempts: Math.max(
227
+ 1,
228
+ input.maxAttempts ??
229
+ defaults?.maxAttempts ??
230
+ getConfigSnapshot().config.runtime.cron.defaultMaxAttempts,
231
+ ),
232
+ retryBackoffMs: Math.max(
233
+ 1_000,
234
+ input.retryBackoffMs ??
235
+ defaults?.retryBackoffMs ??
236
+ getConfigSnapshot().config.runtime.cron.defaultRetryBackoffMs,
237
+ ),
238
+ payload:
239
+ input.payload !== undefined
240
+ ? normalizePayload(input.payload)
241
+ : defaults?.payload ?? {},
242
+ };
243
+ }
244
+
245
+ function resolveWorkspaceRootPath(): string {
246
+ const configured = getConfigSnapshot().config.runtime.opencode.directory?.trim();
247
+ return resolve(configured || process.cwd());
248
+ }
249
+
250
+ export function resolveConditionModuleAbsolutePath(conditionModulePath: string): string {
251
+ const extension = extname(conditionModulePath).toLowerCase();
252
+ if (!CONDITION_MODULE_EXTENSIONS.has(extension)) {
253
+ throw new Error("conditionModulePath must target a .ts, .js, .mjs, or .cjs file");
254
+ }
255
+
256
+ const workspaceRoot = realpathSync(resolveWorkspaceRootPath());
257
+ const target = resolve(workspaceRoot, conditionModulePath);
258
+ let resolvedTarget: string;
259
+ try {
260
+ resolvedTarget = realpathSync(target);
261
+ } catch {
262
+ throw new Error("conditionModulePath must reference an existing file under the runtime workspace directory");
263
+ }
264
+ const relativePath = relative(workspaceRoot, resolvedTarget);
265
+ if (!relativePath || relativePath === ".") {
266
+ throw new Error("conditionModulePath must target a file under the runtime workspace directory");
267
+ }
268
+ if (relativePath.startsWith("..")) {
269
+ throw new Error("conditionModulePath escapes the runtime workspace directory");
270
+ }
271
+ if (!statSync(resolvedTarget).isFile()) {
272
+ throw new Error("conditionModulePath must reference a regular file");
273
+ }
274
+ return resolvedTarget;
275
+ }
276
+
277
+ export function computeDueTimesForDefinition(
278
+ row: {
279
+ schedule_kind: CronScheduleKind;
280
+ schedule_expr: string | null;
281
+ every_ms: number | null;
282
+ at_iso: string | null;
283
+ timezone: string | null;
284
+ last_enqueued_for: number | null;
285
+ created_at: number;
286
+ },
287
+ now: number,
288
+ maxPerTick: number,
289
+ ): number[] {
290
+ const due: number[] = [];
291
+ const lastEnqueued = row.last_enqueued_for ?? null;
292
+
293
+ if (row.schedule_kind === "at") {
294
+ if (!row.at_iso) return due;
295
+ const atMs = Date.parse(row.at_iso);
296
+ if (!Number.isFinite(atMs)) return due;
297
+ if (atMs <= now && (lastEnqueued === null || lastEnqueued < atMs)) {
298
+ due.push(atMs);
299
+ }
300
+ return due;
301
+ }
302
+
303
+ if (row.schedule_kind === "every") {
304
+ const everyMs = row.every_ms ?? 0;
305
+ if (!Number.isFinite(everyMs) || everyMs <= 0) return due;
306
+ let cursor = lastEnqueued ?? row.created_at;
307
+ for (let i = 0; i < maxPerTick; i += 1) {
308
+ const next = cursor + everyMs;
309
+ if (next > now) break;
310
+ due.push(next);
311
+ cursor = next;
312
+ }
313
+ return due;
314
+ }
315
+
316
+ const expr = row.schedule_expr?.trim();
317
+ if (!expr) return due;
318
+ try {
319
+ const cronTime = new CronTime(expr, row.timezone ?? undefined);
320
+ let cursor = lastEnqueued ?? row.created_at - 1_000;
321
+ for (let i = 0; i < maxPerTick; i += 1) {
322
+ const nextDate = cronTime.getNextDateFrom(new Date(cursor));
323
+ const nextMs = nextDate.toMillis();
324
+ if (!Number.isFinite(nextMs)) break;
325
+ if (nextMs > now) break;
326
+ if (nextMs <= cursor) break;
327
+ due.push(nextMs);
328
+ cursor = nextMs + 1_000;
329
+ }
330
+ } catch {
331
+ logger.warn("Ignoring invalid cron expression during scheduling", {
332
+ scheduleExpr: expr,
333
+ timezone: row.timezone,
334
+ });
335
+ }
336
+ return due;
337
+ }
338
+
339
+ export function buildAgentPromptContext(
340
+ definition: CronJobDefinition,
341
+ instance: CronJobInstance,
342
+ context?: Record<string, unknown>,
343
+ ) {
344
+ return {
345
+ ...definitionPayloadContext(definition),
346
+ ...(context ?? {}),
347
+ threadSessionId: definition.threadSessionId,
348
+ instanceId: instance.id,
349
+ scheduledFor: instance.scheduledFor,
350
+ };
351
+ }
@@ -0,0 +1,20 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { env } from "../env";
6
+ import { resolveDataPath } from "../paths";
7
+
8
+ const resolvedDbPath = env.AGENT_MOCKINGBIRD_DB_PATH
9
+ ? path.resolve(env.AGENT_MOCKINGBIRD_DB_PATH)
10
+ : resolveDataPath("agent-mockingbird.db");
11
+
12
+ mkdirSync(path.dirname(resolvedDbPath), { recursive: true });
13
+
14
+ export const sqlite = new Database(resolvedDbPath);
15
+ sqlite.exec("PRAGMA journal_mode=WAL;");
16
+ sqlite.exec("PRAGMA synchronous=NORMAL;");
17
+ sqlite.exec("PRAGMA busy_timeout=5000;");
18
+ sqlite.exec("PRAGMA foreign_keys=ON;");
19
+
20
+ export { resolvedDbPath };
@@ -0,0 +1,40 @@
1
+ import { drizzle } from "drizzle-orm/bun-sqlite";
2
+ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { resolvedDbPath, sqlite } from "./client";
7
+ import * as schema from "./schema";
8
+ import { getBinaryDir } from "../paths";
9
+
10
+ function hasMigrationJournal(candidate: string) {
11
+ return existsSync(path.join(candidate, "meta", "_journal.json"));
12
+ }
13
+
14
+ function resolveMigrationsFolder() {
15
+ const candidates = [
16
+ path.resolve(process.cwd(), "drizzle"),
17
+ path.resolve(import.meta.dir, "../../../../../drizzle"),
18
+ path.resolve(path.dirname(process.execPath), "drizzle"),
19
+ path.resolve(getBinaryDir(), "drizzle"),
20
+ ];
21
+ for (const candidate of candidates) {
22
+ if (hasMigrationJournal(candidate)) {
23
+ return candidate;
24
+ }
25
+ }
26
+
27
+ throw new Error(
28
+ `SQLite migrations are missing meta/_journal.json. Checked: ${candidates.join(", ")}`,
29
+ );
30
+ }
31
+
32
+ const migrationsFolder = resolveMigrationsFolder();
33
+ const migrationDb = drizzle({ client: sqlite, schema });
34
+
35
+ console.log(`Running SQLite migrations from ${migrationsFolder}`);
36
+ console.log(`Target database: ${resolvedDbPath}`);
37
+
38
+ migrate(migrationDb, { migrationsFolder });
39
+
40
+ console.log("Migrations complete");