@lobu/cli 6.0.1 → 7.0.0

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 (265) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +11 -11
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +38 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +574 -40
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +180 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/client.js +308 -28
  10. package/dist/commands/_lib/apply/client.js.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
  12. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  13. package/dist/commands/_lib/apply/desired-state.js +703 -89
  14. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  15. package/dist/commands/_lib/apply/diff.d.ts +61 -3
  16. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  17. package/dist/commands/_lib/apply/diff.js +382 -92
  18. package/dist/commands/_lib/apply/diff.js.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  20. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  21. package/dist/commands/_lib/apply/prompt.js +16 -0
  22. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  23. package/dist/commands/_lib/apply/render.d.ts +9 -0
  24. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  25. package/dist/commands/_lib/apply/render.js +80 -3
  26. package/dist/commands/_lib/apply/render.js.map +1 -1
  27. package/dist/commands/agent.d.ts +7 -0
  28. package/dist/commands/agent.d.ts.map +1 -1
  29. package/dist/commands/agent.js +65 -1
  30. package/dist/commands/agent.js.map +1 -1
  31. package/dist/commands/chat.d.ts +12 -9
  32. package/dist/commands/chat.d.ts.map +1 -1
  33. package/dist/commands/chat.js +125 -57
  34. package/dist/commands/chat.js.map +1 -1
  35. package/dist/commands/dev.d.ts +23 -7
  36. package/dist/commands/dev.d.ts.map +1 -1
  37. package/dist/commands/dev.js +197 -49
  38. package/dist/commands/dev.js.map +1 -1
  39. package/dist/commands/doctor.d.ts +1 -0
  40. package/dist/commands/doctor.d.ts.map +1 -1
  41. package/dist/commands/doctor.js +136 -0
  42. package/dist/commands/doctor.js.map +1 -1
  43. package/dist/commands/eval.d.ts +8 -0
  44. package/dist/commands/eval.d.ts.map +1 -1
  45. package/dist/commands/eval.js +72 -6
  46. package/dist/commands/eval.js.map +1 -1
  47. package/dist/commands/init.d.ts +22 -5
  48. package/dist/commands/init.d.ts.map +1 -1
  49. package/dist/commands/init.js +355 -182
  50. package/dist/commands/init.js.map +1 -1
  51. package/dist/commands/link.d.ts +11 -0
  52. package/dist/commands/link.d.ts.map +1 -0
  53. package/dist/commands/link.js +28 -0
  54. package/dist/commands/link.js.map +1 -0
  55. package/dist/commands/login.d.ts.map +1 -1
  56. package/dist/commands/login.js +14 -2
  57. package/dist/commands/login.js.map +1 -1
  58. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  59. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  60. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  61. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  62. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  63. package/dist/commands/memory/_lib/mcp.js +24 -12
  64. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  65. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  66. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  67. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  68. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  69. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  70. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  71. package/dist/commands/memory/_lib/schema.d.ts +29 -2
  72. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  73. package/dist/commands/memory/_lib/schema.js +121 -5
  74. package/dist/commands/memory/_lib/schema.js.map +1 -1
  75. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  76. package/dist/commands/memory/_lib/seed-cmd.js +46 -24
  77. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  78. package/dist/commands/memory/run.d.ts.map +1 -1
  79. package/dist/commands/memory/run.js +2 -2
  80. package/dist/commands/memory/run.js.map +1 -1
  81. package/dist/commands/org.d.ts +4 -0
  82. package/dist/commands/org.d.ts.map +1 -1
  83. package/dist/commands/org.js +10 -0
  84. package/dist/commands/org.js.map +1 -1
  85. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  86. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  87. package/dist/commands/platforms/platform-prompts.js +54 -8
  88. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  89. package/dist/commands/telemetry.d.ts +10 -0
  90. package/dist/commands/telemetry.d.ts.map +1 -0
  91. package/dist/commands/telemetry.js +68 -0
  92. package/dist/commands/telemetry.js.map +1 -0
  93. package/dist/commands/token.d.ts +9 -0
  94. package/dist/commands/token.d.ts.map +1 -1
  95. package/dist/commands/token.js +54 -0
  96. package/dist/commands/token.js.map +1 -1
  97. package/dist/commands/whoami.d.ts.map +1 -1
  98. package/dist/commands/whoami.js +1 -1
  99. package/dist/commands/whoami.js.map +1 -1
  100. package/dist/connectors/README.md +534 -0
  101. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  102. package/dist/connectors/apple_health.ts +138 -0
  103. package/dist/connectors/apple_screen_time.ts +82 -0
  104. package/dist/connectors/browser-scraper-utils.ts +246 -0
  105. package/dist/connectors/capterra.ts +277 -0
  106. package/dist/connectors/g2.ts +290 -0
  107. package/dist/connectors/github.ts +1530 -0
  108. package/dist/connectors/glassdoor.ts +295 -0
  109. package/dist/connectors/gmaps.ts +197 -0
  110. package/dist/connectors/google_calendar.ts +641 -0
  111. package/dist/connectors/google_gmail.ts +754 -0
  112. package/dist/connectors/google_photos.ts +776 -0
  113. package/dist/connectors/google_play.ts +349 -0
  114. package/dist/connectors/hackernews.ts +471 -0
  115. package/dist/connectors/index.ts +28 -0
  116. package/dist/connectors/ios_appstore.ts +226 -0
  117. package/dist/connectors/linkedin.ts +494 -0
  118. package/dist/connectors/local_directory.ts +91 -0
  119. package/dist/connectors/microsoft_outlook.ts +410 -0
  120. package/dist/connectors/producthunt.ts +471 -0
  121. package/dist/connectors/reddit.ts +600 -0
  122. package/dist/connectors/revolut.ts +572 -0
  123. package/dist/connectors/rss.ts +448 -0
  124. package/dist/connectors/spotify.ts +590 -0
  125. package/dist/connectors/trustpilot.ts +203 -0
  126. package/dist/connectors/website.ts +629 -0
  127. package/dist/connectors/whatsapp.ts +1081 -0
  128. package/dist/connectors/whatsapp_local.ts +125 -0
  129. package/dist/connectors/x.ts +536 -0
  130. package/dist/connectors/youtube.ts +666 -0
  131. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  132. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  133. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  134. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  135. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  136. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  137. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  138. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  139. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  140. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  141. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  142. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  143. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  144. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  145. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  146. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  147. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  148. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  149. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  150. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  151. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  152. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  153. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  154. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  155. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  156. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  157. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  158. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  159. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  160. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  161. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  162. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  163. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  164. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  165. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  166. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  167. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  168. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  169. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  170. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  171. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  172. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  173. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  174. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  175. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  176. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  177. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  178. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  179. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  180. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  181. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  182. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  183. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  184. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  185. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  186. package/dist/eval/types.d.ts +2 -0
  187. package/dist/eval/types.d.ts.map +1 -1
  188. package/dist/index.d.ts +11 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +210 -132
  191. package/dist/index.js.map +1 -1
  192. package/dist/internal/api-client.d.ts +4 -8
  193. package/dist/internal/api-client.d.ts.map +1 -1
  194. package/dist/internal/api-client.js +1 -1
  195. package/dist/internal/api-client.js.map +1 -1
  196. package/dist/internal/context.js +2 -2
  197. package/dist/internal/context.js.map +1 -1
  198. package/dist/internal/credentials.d.ts.map +1 -1
  199. package/dist/internal/credentials.js +6 -1
  200. package/dist/internal/credentials.js.map +1 -1
  201. package/dist/internal/gateway-url.d.ts +14 -0
  202. package/dist/internal/gateway-url.d.ts.map +1 -1
  203. package/dist/internal/gateway-url.js +19 -0
  204. package/dist/internal/gateway-url.js.map +1 -1
  205. package/dist/internal/index.d.ts +3 -4
  206. package/dist/internal/index.d.ts.map +1 -1
  207. package/dist/internal/index.js +3 -3
  208. package/dist/internal/index.js.map +1 -1
  209. package/dist/internal/oauth.d.ts +6 -5
  210. package/dist/internal/oauth.d.ts.map +1 -1
  211. package/dist/internal/oauth.js +2 -2
  212. package/dist/internal/project-link.d.ts +10 -0
  213. package/dist/internal/project-link.d.ts.map +1 -0
  214. package/dist/internal/project-link.js +48 -0
  215. package/dist/internal/project-link.js.map +1 -0
  216. package/dist/providers.json +2 -2
  217. package/dist/server.bundle.mjs +31654 -30866
  218. package/dist/start-local.bundle.mjs +74409 -0
  219. package/dist/templates/README.md.tmpl +10 -11
  220. package/dist/templates/TESTING.md.tmpl +9 -9
  221. package/package.json +15 -13
  222. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  223. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  224. package/dist/__tests__/chat.integration.test.js +0 -337
  225. package/dist/__tests__/chat.integration.test.js.map +0 -1
  226. package/dist/__tests__/dev.test.d.ts +0 -2
  227. package/dist/__tests__/dev.test.d.ts.map +0 -1
  228. package/dist/__tests__/dev.test.js +0 -25
  229. package/dist/__tests__/dev.test.js.map +0 -1
  230. package/dist/__tests__/init-memory.test.d.ts +0 -2
  231. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  232. package/dist/__tests__/init-memory.test.js +0 -45
  233. package/dist/__tests__/init-memory.test.js.map +0 -1
  234. package/dist/__tests__/token.test.d.ts +0 -2
  235. package/dist/__tests__/token.test.d.ts.map +0 -1
  236. package/dist/__tests__/token.test.js +0 -52
  237. package/dist/__tests__/token.test.js.map +0 -1
  238. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  239. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  240. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  241. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  242. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  243. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  244. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  245. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  246. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  247. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  248. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  249. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  250. package/dist/commands/apply.d.ts +0 -3
  251. package/dist/commands/apply.d.ts.map +0 -1
  252. package/dist/commands/apply.js +0 -5
  253. package/dist/commands/apply.js.map +0 -1
  254. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  255. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  256. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  257. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  258. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  259. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  260. package/dist/internal/__tests__/api-client.test.js +0 -95
  261. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  262. package/dist/internal/__tests__/context.test.d.ts +0 -2
  263. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  264. package/dist/internal/__tests__/context.test.js +0 -77
  265. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,97 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 6: NOTIFY trigger for invalidatableCache invalidation.
4
+ --
5
+ -- The agent-related runtime caches (formerly Redis-backed) read agents,
6
+ -- channel bindings, user-agent associations, and per-(user,agent) auth
7
+ -- profiles directly from Postgres. Each gateway process keeps a small
8
+ -- read-through cache invalidated by `pg_notify('agent_changed', <key>)`.
9
+ --
10
+ -- We emit a single channel name and let the cache implementation match
11
+ -- the payload against the cached key. Channels are deliberately coarse
12
+ -- (one per logical resource family) to keep the postmaster's notification
13
+ -- table small — see invalidatable-cache.ts.
14
+
15
+ CREATE OR REPLACE FUNCTION public.notify_agent_changed()
16
+ RETURNS trigger
17
+ LANGUAGE plpgsql AS $$
18
+ BEGIN
19
+ PERFORM pg_notify('agent_changed', COALESCE(NEW.id, OLD.id));
20
+ RETURN COALESCE(NEW, OLD);
21
+ END;
22
+ $$;
23
+
24
+ DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents;
25
+ CREATE TRIGGER agents_changed_notify
26
+ AFTER INSERT OR UPDATE OR DELETE ON public.agents
27
+ FOR EACH ROW EXECUTE FUNCTION public.notify_agent_changed();
28
+
29
+
30
+ -- Channel bindings: payload is "<platform>:<teamId|->:<channelId>" so the
31
+ -- in-process cache (keyed identically) can drop just the affected entry.
32
+ CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed()
33
+ RETURNS trigger
34
+ LANGUAGE plpgsql AS $$
35
+ DECLARE
36
+ rec record;
37
+ payload text;
38
+ BEGIN
39
+ IF TG_OP = 'DELETE' THEN
40
+ rec := OLD;
41
+ ELSE
42
+ rec := NEW;
43
+ END IF;
44
+ payload := format(
45
+ '%s:%s:%s',
46
+ rec.platform,
47
+ COALESCE(rec.team_id, '-'),
48
+ rec.channel_id
49
+ );
50
+ PERFORM pg_notify('channel_binding_changed', payload);
51
+ RETURN COALESCE(NEW, OLD);
52
+ END;
53
+ $$;
54
+
55
+ DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings;
56
+ CREATE TRIGGER agent_channel_bindings_changed_notify
57
+ AFTER INSERT OR UPDATE OR DELETE ON public.agent_channel_bindings
58
+ FOR EACH ROW EXECUTE FUNCTION public.notify_channel_binding_changed();
59
+
60
+
61
+ -- User-agent associations: payload is "<platform>:<userId>" so the
62
+ -- agent listing cache for a single user can be dropped without affecting
63
+ -- other users.
64
+ CREATE OR REPLACE FUNCTION public.notify_agent_users_changed()
65
+ RETURNS trigger
66
+ LANGUAGE plpgsql AS $$
67
+ DECLARE
68
+ rec record;
69
+ BEGIN
70
+ IF TG_OP = 'DELETE' THEN
71
+ rec := OLD;
72
+ ELSE
73
+ rec := NEW;
74
+ END IF;
75
+ PERFORM pg_notify(
76
+ 'agent_users_changed',
77
+ format('%s:%s', rec.platform, rec.user_id)
78
+ );
79
+ RETURN COALESCE(NEW, OLD);
80
+ END;
81
+ $$;
82
+
83
+ DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users;
84
+ CREATE TRIGGER agent_users_changed_notify
85
+ AFTER INSERT OR UPDATE OR DELETE ON public.agent_users
86
+ FOR EACH ROW EXECUTE FUNCTION public.notify_agent_users_changed();
87
+
88
+ -- migrate:down
89
+
90
+ DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users;
91
+ DROP FUNCTION IF EXISTS public.notify_agent_users_changed();
92
+
93
+ DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings;
94
+ DROP FUNCTION IF EXISTS public.notify_channel_binding_changed();
95
+
96
+ DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents;
97
+ DROP FUNCTION IF EXISTS public.notify_agent_changed();
@@ -0,0 +1,36 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 6: PG-backed storage for per-user runtime state previously held in Redis.
4
+ --
5
+ -- These tables replace Redis-keyed structures:
6
+ -- user:auth-profiles:{userId}:{agentId} → user_auth_profiles
7
+ -- {providerId}:model_preference:{userId} → user_model_preferences
8
+ --
9
+ -- The previous Redis layout kept the credential/refreshToken in the secret
10
+ -- store and persisted only refs in the cached JSON; we keep that contract
11
+ -- and store the same JSON document in `profiles` here.
12
+
13
+ CREATE TABLE IF NOT EXISTS public.user_auth_profiles (
14
+ user_id text NOT NULL,
15
+ agent_id text NOT NULL,
16
+ profiles jsonb DEFAULT '[]'::jsonb NOT NULL,
17
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
18
+ PRIMARY KEY (user_id, agent_id)
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS user_auth_profiles_agent_id_idx
22
+ ON public.user_auth_profiles (agent_id);
23
+
24
+
25
+ CREATE TABLE IF NOT EXISTS public.user_model_preferences (
26
+ user_id text NOT NULL,
27
+ provider_id text NOT NULL,
28
+ model text NOT NULL,
29
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
30
+ PRIMARY KEY (user_id, provider_id)
31
+ );
32
+
33
+ -- migrate:down
34
+
35
+ DROP TABLE IF EXISTS public.user_model_preferences;
36
+ DROP TABLE IF EXISTS public.user_auth_profiles;
@@ -0,0 +1,130 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 6 follow-up: emit BOTH the OLD and NEW cache keys when a row's key
4
+ -- columns change in an UPDATE. The Phase-6 triggers only emitted the NEW
5
+ -- key, so a process that had the OLD key cached would miss the invalidation
6
+ -- and serve stale data until the TTL expired.
7
+
8
+ -- Phase 6 follow-up: add a partial unique index that treats NULL team_id as
9
+ -- a single equivalence class. Postgres unique constraints treat NULLs as
10
+ -- distinct, so the existing UNIQUE (platform, channel_id, team_id) lets
11
+ -- repeated upserts of (platform, channel_id, NULL) insert duplicate rows;
12
+ -- the matching ON CONFLICT clause never fires and a subsequent getBinding()
13
+ -- reads an arbitrary row.
14
+ --
15
+ -- Using a partial index lets the existing platform/channel_id/team_id
16
+ -- constraint stay in place for the team_id-set rows; the new index covers
17
+ -- the team_id-null branch.
18
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_channel_bindings_no_team_unique
19
+ ON public.agent_channel_bindings (platform, channel_id)
20
+ WHERE team_id IS NULL;
21
+
22
+
23
+ CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed()
24
+ RETURNS trigger
25
+ LANGUAGE plpgsql AS $$
26
+ DECLARE
27
+ new_payload text;
28
+ old_payload text;
29
+ BEGIN
30
+ IF TG_OP = 'DELETE' THEN
31
+ PERFORM pg_notify(
32
+ 'channel_binding_changed',
33
+ format('%s:%s:%s', OLD.platform, COALESCE(OLD.team_id, '-'), OLD.channel_id)
34
+ );
35
+ RETURN OLD;
36
+ END IF;
37
+
38
+ new_payload := format(
39
+ '%s:%s:%s', NEW.platform, COALESCE(NEW.team_id, '-'), NEW.channel_id
40
+ );
41
+ PERFORM pg_notify('channel_binding_changed', new_payload);
42
+
43
+ IF TG_OP = 'UPDATE' THEN
44
+ old_payload := format(
45
+ '%s:%s:%s', OLD.platform, COALESCE(OLD.team_id, '-'), OLD.channel_id
46
+ );
47
+ IF old_payload <> new_payload THEN
48
+ PERFORM pg_notify('channel_binding_changed', old_payload);
49
+ END IF;
50
+ END IF;
51
+
52
+ RETURN NEW;
53
+ END;
54
+ $$;
55
+
56
+
57
+ CREATE OR REPLACE FUNCTION public.notify_agent_users_changed()
58
+ RETURNS trigger
59
+ LANGUAGE plpgsql AS $$
60
+ DECLARE
61
+ new_payload text;
62
+ old_payload text;
63
+ BEGIN
64
+ IF TG_OP = 'DELETE' THEN
65
+ PERFORM pg_notify(
66
+ 'agent_users_changed', format('%s:%s', OLD.platform, OLD.user_id)
67
+ );
68
+ RETURN OLD;
69
+ END IF;
70
+
71
+ new_payload := format('%s:%s', NEW.platform, NEW.user_id);
72
+ PERFORM pg_notify('agent_users_changed', new_payload);
73
+
74
+ IF TG_OP = 'UPDATE' THEN
75
+ old_payload := format('%s:%s', OLD.platform, OLD.user_id);
76
+ IF old_payload <> new_payload THEN
77
+ PERFORM pg_notify('agent_users_changed', old_payload);
78
+ END IF;
79
+ END IF;
80
+
81
+ RETURN NEW;
82
+ END;
83
+ $$;
84
+
85
+ -- migrate:down
86
+
87
+ DROP INDEX IF EXISTS public.agent_channel_bindings_no_team_unique;
88
+
89
+ -- Restore the Phase-6 (pre-fix) function bodies, which only emitted the NEW key.
90
+ CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed()
91
+ RETURNS trigger
92
+ LANGUAGE plpgsql AS $$
93
+ DECLARE
94
+ rec record;
95
+ payload text;
96
+ BEGIN
97
+ IF TG_OP = 'DELETE' THEN
98
+ rec := OLD;
99
+ ELSE
100
+ rec := NEW;
101
+ END IF;
102
+ payload := format(
103
+ '%s:%s:%s',
104
+ rec.platform,
105
+ COALESCE(rec.team_id, '-'),
106
+ rec.channel_id
107
+ );
108
+ PERFORM pg_notify('channel_binding_changed', payload);
109
+ RETURN COALESCE(NEW, OLD);
110
+ END;
111
+ $$;
112
+
113
+ CREATE OR REPLACE FUNCTION public.notify_agent_users_changed()
114
+ RETURNS trigger
115
+ LANGUAGE plpgsql AS $$
116
+ DECLARE
117
+ rec record;
118
+ BEGIN
119
+ IF TG_OP = 'DELETE' THEN
120
+ rec := OLD;
121
+ ELSE
122
+ rec := NEW;
123
+ END IF;
124
+ PERFORM pg_notify(
125
+ 'agent_users_changed',
126
+ format('%s:%s', rec.platform, rec.user_id)
127
+ );
128
+ RETURN COALESCE(NEW, OLD);
129
+ END;
130
+ $$;
@@ -0,0 +1,83 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 7 of Redis -> Postgres migration: replace the three remaining
4
+ -- ephemeral-key Redis stores (OAuth state CSRF nonces, CLI auth sessions,
5
+ -- and the fixed-window rate limiter) with thin Postgres tables. None of
6
+ -- these need cross-process pub/sub or pipelining; they're cheap row-level
7
+ -- reads/writes with a TTL column for lazy cleanup on read plus a periodic
8
+ -- vacuum task.
9
+ --
10
+ -- Design notes:
11
+ -- - All three tables key on a text id and store an explicit `expires_at`.
12
+ -- Lazy reads filter `expires_at > now()`; a background sweeper deletes
13
+ -- stale rows in bulk.
14
+ -- - `payload` is jsonb so the OAuth state stores can keep their schema
15
+ -- flexible (PKCE verifier, redirect URI, MCP discovery context, etc.)
16
+ -- without churning migrations.
17
+ -- - The rate_limits table is a single counter per (key, window_started_at)
18
+ -- instead of one row per request — the existing Redis impl is a fixed
19
+ -- window with INCR + EXPIRE, so a counter row matches the semantics
20
+ -- exactly.
21
+
22
+ -- OAuth state nonces: provider PKCE flows, MCP OAuth flow, Slack install,
23
+ -- CLI browser/device handoff. `scope` mirrors the Redis key-prefix
24
+ -- (e.g. `claude:oauth_state`, `mcp-oauth:state`, `slack:oauth:state`,
25
+ -- `cli:auth:request`) so a single lookup tagged by scope+id replaces the
26
+ -- previous prefix+id Redis lookup.
27
+ CREATE TABLE IF NOT EXISTS public.oauth_states (
28
+ id text PRIMARY KEY,
29
+ scope text NOT NULL,
30
+ payload jsonb NOT NULL,
31
+ expires_at timestamptz NOT NULL,
32
+ created_at timestamptz NOT NULL DEFAULT now()
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS oauth_states_scope_idx
36
+ ON public.oauth_states (scope);
37
+
38
+ CREATE INDEX IF NOT EXISTS oauth_states_expires_at_idx
39
+ ON public.oauth_states (expires_at);
40
+
41
+ -- CLI auth sessions for the `lobu` CLI. Each row is a long-lived (30 day)
42
+ -- refresh-token-anchored session; the access token is JWT-shaped and
43
+ -- carries `sessionId` so verifyAccessToken can re-check the row exists
44
+ -- and hasn't been revoked.
45
+ CREATE TABLE IF NOT EXISTS public.cli_sessions (
46
+ session_id text PRIMARY KEY,
47
+ user_id text NOT NULL,
48
+ email text,
49
+ name text,
50
+ refresh_token_id text NOT NULL,
51
+ expires_at timestamptz NOT NULL,
52
+ created_at timestamptz NOT NULL DEFAULT now()
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS cli_sessions_user_id_idx
56
+ ON public.cli_sessions (user_id);
57
+
58
+ CREATE INDEX IF NOT EXISTS cli_sessions_expires_at_idx
59
+ ON public.cli_sessions (expires_at);
60
+
61
+ -- Fixed-window rate limit counters. One row per (key, window_started_at);
62
+ -- a successful consume() does an UPSERT that increments `count`, sets the
63
+ -- window if missing, and returns the new count. The window expires when
64
+ -- `expires_at <= now()`.
65
+ --
66
+ -- `key` is the same string the Redis impl used (`rate-limit:cli:admin-login:<ip>`,
67
+ -- etc) so callers don't have to translate.
68
+ CREATE TABLE IF NOT EXISTS public.rate_limits (
69
+ key text PRIMARY KEY,
70
+ count integer NOT NULL DEFAULT 0,
71
+ window_started_at timestamptz NOT NULL,
72
+ expires_at timestamptz NOT NULL,
73
+ updated_at timestamptz NOT NULL DEFAULT now()
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS rate_limits_expires_at_idx
77
+ ON public.rate_limits (expires_at);
78
+
79
+ -- migrate:down
80
+
81
+ DROP TABLE IF EXISTS public.rate_limits;
82
+ DROP TABLE IF EXISTS public.cli_sessions;
83
+ DROP TABLE IF EXISTS public.oauth_states;
@@ -0,0 +1,84 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 8 of Redis -> Postgres migration: replace the remaining Redis-only
4
+ -- substrates that don't fit cleanly into the existing tables with proper
5
+ -- typed Postgres tables.
6
+ --
7
+ -- 1. `grants` — per-(agent, kind, pattern) grant rows. Replaces the
8
+ -- `grant:<agentId>:<pattern>` Redis key prefix and SCAN-by-prefix list.
9
+ -- Wildcard expansion happens in the application layer.
10
+ -- 2. `chat_connections` — chat-platform (Telegram/Slack/Discord/...) connection
11
+ -- rows. Replaces the `connection:<id>`, `connections:all`, and
12
+ -- `connections:agent:<id>` Redis keys used by ChatInstanceManager. The
13
+ -- existing `public.connections` table is for Lobu product connectors,
14
+ -- not chat platforms.
15
+ -- 3. `mcp_proxy_sessions` — short-lived MCP server session-id mappings used
16
+ -- by the MCP proxy. The existing `public.mcp_sessions` table is the
17
+ -- inbound MCP-server-as-server session table; this is the outbound
18
+ -- upstream-MCP session-id cache.
19
+
20
+ -- ============================================================================
21
+ -- grants
22
+ -- ============================================================================
23
+
24
+ CREATE TABLE IF NOT EXISTS public.grants (
25
+ agent_id text NOT NULL REFERENCES public.agents(id) ON DELETE CASCADE,
26
+ kind text NOT NULL,
27
+ pattern text NOT NULL,
28
+ expires_at timestamptz,
29
+ granted_at timestamptz NOT NULL DEFAULT now(),
30
+ denied boolean NOT NULL DEFAULT false,
31
+ PRIMARY KEY (agent_id, kind, pattern)
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS grants_agent_id_idx
35
+ ON public.grants (agent_id);
36
+
37
+ CREATE INDEX IF NOT EXISTS grants_expires_at_idx
38
+ ON public.grants (expires_at)
39
+ WHERE expires_at IS NOT NULL;
40
+
41
+ -- ============================================================================
42
+ -- chat_connections
43
+ -- ============================================================================
44
+
45
+ CREATE TABLE IF NOT EXISTS public.chat_connections (
46
+ id text PRIMARY KEY,
47
+ platform text NOT NULL,
48
+ template_agent_id text REFERENCES public.agents(id) ON DELETE CASCADE,
49
+ config jsonb NOT NULL,
50
+ settings jsonb NOT NULL DEFAULT '{}'::jsonb,
51
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
52
+ status text NOT NULL DEFAULT 'active',
53
+ error_message text,
54
+ created_at timestamptz NOT NULL DEFAULT now(),
55
+ updated_at timestamptz NOT NULL DEFAULT now()
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS chat_connections_template_agent_id_idx
59
+ ON public.chat_connections (template_agent_id)
60
+ WHERE template_agent_id IS NOT NULL;
61
+
62
+ CREATE INDEX IF NOT EXISTS chat_connections_platform_idx
63
+ ON public.chat_connections (platform);
64
+
65
+ -- ============================================================================
66
+ -- mcp_proxy_sessions (NOT to be confused with public.mcp_sessions which is
67
+ -- the inbound MCP server's session table)
68
+ -- ============================================================================
69
+
70
+ CREATE TABLE IF NOT EXISTS public.mcp_proxy_sessions (
71
+ session_key text PRIMARY KEY,
72
+ upstream_session_id text NOT NULL,
73
+ expires_at timestamptz NOT NULL,
74
+ updated_at timestamptz NOT NULL DEFAULT now()
75
+ );
76
+
77
+ CREATE INDEX IF NOT EXISTS mcp_proxy_sessions_expires_at_idx
78
+ ON public.mcp_proxy_sessions (expires_at);
79
+
80
+ -- migrate:down
81
+
82
+ DROP TABLE IF EXISTS public.mcp_proxy_sessions;
83
+ DROP TABLE IF EXISTS public.chat_connections;
84
+ DROP TABLE IF EXISTS public.grants;
@@ -0,0 +1,44 @@
1
+ -- migrate:up
2
+
3
+ -- Phase 10 of Redis -> Postgres migration: honor the caller-supplied
4
+ -- queue options (priority, expireInSeconds, retryDelay) that RunsQueue
5
+ -- previously dropped on the floor.
6
+ --
7
+ -- 1. priority: int, default 0; claim ORDER BY priority DESC, run_at ASC, id ASC.
8
+ -- 2. expires_at: row-level TTL. Claim filter excludes expired rows; the
9
+ -- periodic cleanup task deletes them.
10
+ -- 3. retry_delay_seconds: when set, scheduleRetry uses fixed-delay backoff
11
+ -- instead of exponential. NULL falls back to the existing exponential
12
+ -- cap-300s curve.
13
+
14
+ ALTER TABLE public.runs
15
+ ADD COLUMN IF NOT EXISTS priority integer NOT NULL DEFAULT 0,
16
+ ADD COLUMN IF NOT EXISTS expires_at timestamptz,
17
+ ADD COLUMN IF NOT EXISTS retry_delay_seconds integer;
18
+
19
+ -- Refresh the lobu-claim index so priority + run_at decide claim order.
20
+ DROP INDEX IF EXISTS public.runs_lobu_claim_idx;
21
+
22
+ CREATE INDEX IF NOT EXISTS runs_lobu_claim_idx
23
+ ON public.runs (run_type, queue_name, priority DESC, run_at ASC, id ASC)
24
+ WHERE status = 'pending'
25
+ AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal');
26
+
27
+ CREATE INDEX IF NOT EXISTS runs_expires_at_idx
28
+ ON public.runs (expires_at)
29
+ WHERE expires_at IS NOT NULL;
30
+
31
+ -- migrate:down
32
+
33
+ DROP INDEX IF EXISTS public.runs_expires_at_idx;
34
+ DROP INDEX IF EXISTS public.runs_lobu_claim_idx;
35
+
36
+ CREATE INDEX IF NOT EXISTS runs_lobu_claim_idx
37
+ ON public.runs (run_type, queue_name, run_at)
38
+ WHERE status = 'pending'
39
+ AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal');
40
+
41
+ ALTER TABLE public.runs
42
+ DROP COLUMN IF EXISTS retry_delay_seconds,
43
+ DROP COLUMN IF EXISTS expires_at,
44
+ DROP COLUMN IF EXISTS priority;
@@ -0,0 +1,25 @@
1
+ -- migrate:up
2
+
3
+ -- Drop the NOTIFY triggers + functions that powered the InvalidatableCache.
4
+ -- The cache layer was removed: stores read-through to PG directly. Reads sit
5
+ -- at ~7 SELECTs per chat dispatch — well within PG capacity at current scale.
6
+ -- The runs-queue's pg_notify('runs_lobu:<queue>', ...) wakeup path is
7
+ -- unaffected (different channel, different trigger).
8
+
9
+ DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users;
10
+ DROP FUNCTION IF EXISTS public.notify_agent_users_changed();
11
+
12
+ DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings;
13
+ DROP FUNCTION IF EXISTS public.notify_channel_binding_changed();
14
+
15
+ DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents;
16
+ DROP FUNCTION IF EXISTS public.notify_agent_changed();
17
+
18
+ -- migrate:down
19
+
20
+ -- Restoration mirrors 20260429120000_agent_changed_notify.sql +
21
+ -- 20260429120200_fix_notify_old_keys.sql; if you need to roll back, replay
22
+ -- those by hand. We don't reproduce them here because the cache that consumed
23
+ -- these channels no longer exists — a rollback to the old behavior requires
24
+ -- restoring the cache code too.
25
+ SELECT 1;
@@ -0,0 +1,21 @@
1
+ -- migrate:up
2
+
3
+ -- Persist three agent settings fields that the file-loader produces from
4
+ -- lobu.toml but the postgres-backed AgentConfigStore had nowhere to put:
5
+ -- * egress_config -> AgentSettings.egressConfig
6
+ -- * pre_approved_tools -> AgentSettings.preApprovedTools
7
+ -- * guardrails -> AgentSettings.guardrails
8
+ -- Without these columns, `lobu apply` would silently drop the values on
9
+ -- every push, producing perpetual drift between local and cloud.
10
+
11
+ ALTER TABLE public.agents
12
+ ADD COLUMN egress_config jsonb DEFAULT '{}'::jsonb,
13
+ ADD COLUMN pre_approved_tools jsonb DEFAULT '[]'::jsonb,
14
+ ADD COLUMN guardrails jsonb DEFAULT '[]'::jsonb;
15
+
16
+ -- migrate:down
17
+
18
+ ALTER TABLE public.agents
19
+ DROP COLUMN egress_config,
20
+ DROP COLUMN pre_approved_tools,
21
+ DROP COLUMN guardrails;
@@ -0,0 +1,69 @@
1
+ -- migrate:up
2
+
3
+ -- Fix connection-config encryption asymmetry in agent_connections.config.
4
+ --
5
+ -- encryptConfig() in postgres-stores.ts historically returned raw
6
+ -- "iv:tag:ciphertext" output from @lobu/core's `encrypt()`, but
7
+ -- decryptConfig() only decrypts strings that start with "enc:v1:". So any
8
+ -- secret-named field that hit encryptConfig was stored as prefixless
9
+ -- ciphertext and round-tripped as that ciphertext literal on read.
10
+ --
11
+ -- This migration backfills existing prefixless rows by re-prefixing them so
12
+ -- the now-aligned decryptConfig path can decrypt them.
13
+ --
14
+ -- Identification: AES-GCM in @lobu/core uses a 12-byte IV (24 hex chars)
15
+ -- and a 16-byte auth tag (32 hex chars), joined with the ciphertext as
16
+ -- `iv:tag:ciphertext`. We match exactly that shape to avoid touching
17
+ -- arbitrary `:` separated values.
18
+ --
19
+ -- Idempotent: jsonb_object_agg only rewrites string values that match the
20
+ -- prefixless shape AND lack the prefix. Re-running the migration is a noop.
21
+
22
+ UPDATE public.agent_connections AS ac
23
+ SET config = sub.fixed_config
24
+ FROM (
25
+ SELECT
26
+ id,
27
+ jsonb_object_agg(
28
+ key,
29
+ CASE
30
+ WHEN jsonb_typeof(value) = 'string'
31
+ AND value #>> '{}' ~ '^[0-9a-f]{24}:[0-9a-f]{32}:[0-9a-f]+$'
32
+ AND value #>> '{}' NOT LIKE 'enc:v1:%'
33
+ THEN to_jsonb('enc:v1:' || (value #>> '{}'))
34
+ ELSE value
35
+ END
36
+ ) AS fixed_config
37
+ FROM public.agent_connections,
38
+ LATERAL jsonb_each(config)
39
+ GROUP BY id
40
+ ) AS sub
41
+ WHERE ac.id = sub.id
42
+ AND ac.config IS DISTINCT FROM sub.fixed_config;
43
+
44
+ -- migrate:down
45
+
46
+ -- Strip the "enc:v1:" prefix to restore the prefixless ciphertext shape.
47
+ -- Same regex: only touch strings whose remainder is `iv:tag:ciphertext`.
48
+
49
+ UPDATE public.agent_connections AS ac
50
+ SET config = sub.fixed_config
51
+ FROM (
52
+ SELECT
53
+ id,
54
+ jsonb_object_agg(
55
+ key,
56
+ CASE
57
+ WHEN jsonb_typeof(value) = 'string'
58
+ AND value #>> '{}' LIKE 'enc:v1:%'
59
+ AND substring(value #>> '{}' FROM 8) ~ '^[0-9a-f]{24}:[0-9a-f]{32}:[0-9a-f]+$'
60
+ THEN to_jsonb(substring(value #>> '{}' FROM 8))
61
+ ELSE value
62
+ END
63
+ ) AS fixed_config
64
+ FROM public.agent_connections,
65
+ LATERAL jsonb_each(config)
66
+ GROUP BY id
67
+ ) AS sub
68
+ WHERE ac.id = sub.id
69
+ AND ac.config IS DISTINCT FROM sub.fixed_config;
@@ -0,0 +1,77 @@
1
+ -- migrate:up transaction:false
2
+
3
+ -- Add 'task' to the runs_run_type_check + extend runs_lobu_claim_idx.
4
+ --
5
+ -- Background: the lobu-queue lanes (chat_message, schedule, agent_run,
6
+ -- internal) are claimed in-process by the gateway's RunsQueue. The 'task'
7
+ -- lane extends that pattern for platform-side periodic + lazy work
8
+ -- (token refresh, classification reconciliation, embed backfill, watcher
9
+ -- maintenance, etc.) that previously ran from a single setInterval-driven
10
+ -- maintenance scheduler.
11
+ --
12
+ -- Lock-safety: this migration runs `transaction:false` so that
13
+ -- CREATE INDEX CONCURRENTLY and VALIDATE CONSTRAINT release locks between
14
+ -- statements. Without it, dbmate's per-migration transaction would force
15
+ -- ACCESS EXCLUSIVE on the runs table for the duration of a constraint
16
+ -- validation or index build — visible downtime for a hot queue table.
17
+ SET lock_timeout = '5s';
18
+
19
+ -- 1. Widen the run_type CHECK constraint without scanning the table.
20
+ -- NOT VALID adds the catalog row under a brief ACCESS EXCLUSIVE.
21
+ -- VALIDATE takes only SHARE UPDATE EXCLUSIVE so concurrent reads/writes
22
+ -- are unaffected. Idempotent: re-runs safely if a prior run died midway.
23
+ DO $$
24
+ BEGIN
25
+ IF NOT EXISTS (
26
+ SELECT 1
27
+ FROM pg_constraint
28
+ WHERE conrelid = 'public.runs'::regclass
29
+ AND conname = 'runs_run_type_check_v2'
30
+ ) THEN
31
+ ALTER TABLE public.runs
32
+ ADD CONSTRAINT runs_run_type_check_v2 CHECK (run_type = ANY (ARRAY[
33
+ 'sync'::text,
34
+ 'action'::text,
35
+ 'embed_backfill'::text,
36
+ 'watcher'::text,
37
+ 'auth'::text,
38
+ 'chat_message'::text,
39
+ 'schedule'::text,
40
+ 'agent_run'::text,
41
+ 'internal'::text,
42
+ 'task'::text
43
+ ])) NOT VALID;
44
+ END IF;
45
+ END$$;
46
+
47
+ ALTER TABLE public.runs VALIDATE CONSTRAINT runs_run_type_check_v2;
48
+
49
+ ALTER TABLE public.runs DROP CONSTRAINT IF EXISTS runs_run_type_check;
50
+ ALTER TABLE public.runs RENAME CONSTRAINT runs_run_type_check_v2 TO runs_run_type_check;
51
+
52
+ -- 2. Replace the lobu claim index. Originally written with CONCURRENTLY,
53
+ -- but dbmate's transaction wrapper still presents these to PG as
54
+ -- in-transaction even with `transaction:false`, which breaks
55
+ -- CREATE/DROP INDEX CONCURRENTLY (see comments in
56
+ -- 20260426130001_db_integrity_cleanup_concurrent.sql for the same
57
+ -- workaround). The partial index only covers `status = 'pending'`
58
+ -- rows in the lobu lanes — typically a small set since pending rows
59
+ -- are claimed within milliseconds — so the ACCESS EXCLUSIVE held
60
+ -- during the non-concurrent build is sub-second in practice.
61
+ DROP INDEX IF EXISTS public.runs_lobu_claim_idx;
62
+
63
+ CREATE INDEX runs_lobu_claim_idx
64
+ ON public.runs (run_type, queue_name, priority DESC, run_at ASC, id ASC)
65
+ WHERE status = 'pending'
66
+ AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal', 'task');
67
+
68
+ -- migrate:down
69
+
70
+ -- This migration is forward-only.
71
+ --
72
+ -- Reverting would require either deleting all rows with run_type='task'
73
+ -- (data loss — both pending tasks and historical run records) or leaving
74
+ -- the constraint widened (the failure mode the up migration was avoiding).
75
+ -- Neither is safe to do automatically. If you genuinely need to revert,
76
+ -- write a follow-up migration that explicitly handles the data first.
77
+ SELECT 1;