@lobu/cli 6.0.0 → 6.1.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 (222) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +12 -12
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +2 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +26 -0
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +1 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.js +6 -6
  10. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  11. package/dist/commands/agent.d.ts +7 -0
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +65 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/chat.d.ts +12 -9
  16. package/dist/commands/chat.d.ts.map +1 -1
  17. package/dist/commands/chat.js +117 -56
  18. package/dist/commands/chat.js.map +1 -1
  19. package/dist/commands/dev.d.ts +15 -7
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +79 -44
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +1 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +136 -0
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/eval.d.ts +8 -0
  28. package/dist/commands/eval.d.ts.map +1 -1
  29. package/dist/commands/eval.js +56 -1
  30. package/dist/commands/eval.js.map +1 -1
  31. package/dist/commands/init.d.ts +20 -5
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +332 -183
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/link.d.ts +11 -0
  36. package/dist/commands/link.d.ts.map +1 -0
  37. package/dist/commands/link.js +28 -0
  38. package/dist/commands/link.js.map +1 -0
  39. package/dist/commands/login.d.ts.map +1 -1
  40. package/dist/commands/login.js +14 -2
  41. package/dist/commands/login.js.map +1 -1
  42. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  43. package/dist/commands/memory/_lib/browser-auth-cmd.js +4 -4
  44. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  45. package/dist/commands/memory/_lib/install-targets.d.ts.map +1 -1
  46. package/dist/commands/memory/_lib/install-targets.js +1 -5
  47. package/dist/commands/memory/_lib/install-targets.js.map +1 -1
  48. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  49. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  50. package/dist/commands/memory/_lib/mcp.js +24 -12
  51. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  52. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  53. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  54. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  55. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  56. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  57. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  58. package/dist/commands/memory/_lib/schema.d.ts +2 -2
  59. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  60. package/dist/commands/memory/_lib/schema.js +3 -3
  61. package/dist/commands/memory/_lib/schema.js.map +1 -1
  62. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  63. package/dist/commands/memory/_lib/seed-cmd.js +5 -6
  64. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  65. package/dist/commands/memory/run.d.ts.map +1 -1
  66. package/dist/commands/memory/run.js +2 -2
  67. package/dist/commands/memory/run.js.map +1 -1
  68. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  69. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  70. package/dist/commands/platforms/platform-prompts.js +54 -8
  71. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  72. package/dist/commands/telemetry.d.ts +10 -0
  73. package/dist/commands/telemetry.d.ts.map +1 -0
  74. package/dist/commands/telemetry.js +68 -0
  75. package/dist/commands/telemetry.js.map +1 -0
  76. package/dist/commands/whoami.d.ts.map +1 -1
  77. package/dist/commands/whoami.js +1 -1
  78. package/dist/commands/whoami.js.map +1 -1
  79. package/dist/connectors/README.md +534 -0
  80. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  81. package/dist/connectors/browser-scraper-utils.ts +214 -0
  82. package/dist/connectors/capterra.ts +273 -0
  83. package/dist/connectors/g2.ts +286 -0
  84. package/dist/connectors/github.ts +1553 -0
  85. package/dist/connectors/glassdoor.ts +291 -0
  86. package/dist/connectors/gmaps.ts +197 -0
  87. package/dist/connectors/google_calendar.ts +631 -0
  88. package/dist/connectors/google_gmail.ts +751 -0
  89. package/dist/connectors/google_photos.ts +776 -0
  90. package/dist/connectors/google_play.ts +342 -0
  91. package/dist/connectors/hackernews.ts +471 -0
  92. package/dist/connectors/index.ts +23 -0
  93. package/dist/connectors/ios_appstore.ts +226 -0
  94. package/dist/connectors/linkedin.ts +471 -0
  95. package/dist/connectors/microsoft_outlook.ts +410 -0
  96. package/dist/connectors/producthunt.ts +471 -0
  97. package/dist/connectors/reddit.ts +600 -0
  98. package/dist/connectors/rss.ts +448 -0
  99. package/dist/connectors/spotify.ts +590 -0
  100. package/dist/connectors/trustpilot.ts +199 -0
  101. package/dist/connectors/website.ts +629 -0
  102. package/dist/connectors/whatsapp.ts +1073 -0
  103. package/dist/connectors/x.ts +526 -0
  104. package/dist/connectors/youtube.ts +666 -0
  105. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  106. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  107. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  108. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  109. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  110. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  111. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  112. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  113. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  114. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  115. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  116. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  117. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  118. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  119. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  120. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  121. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  122. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  123. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  124. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  125. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  126. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  127. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  128. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  129. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  130. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  131. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  132. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  133. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  134. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  135. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  136. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  137. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  138. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  139. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  140. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  141. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  142. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  143. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  144. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  145. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  146. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  147. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  148. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  149. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  150. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +147 -23
  153. package/dist/index.js.map +1 -1
  154. package/dist/internal/api-client.d.ts +4 -8
  155. package/dist/internal/api-client.d.ts.map +1 -1
  156. package/dist/internal/api-client.js +1 -1
  157. package/dist/internal/api-client.js.map +1 -1
  158. package/dist/internal/context.js +2 -2
  159. package/dist/internal/context.js.map +1 -1
  160. package/dist/internal/credentials.d.ts.map +1 -1
  161. package/dist/internal/credentials.js +6 -1
  162. package/dist/internal/credentials.js.map +1 -1
  163. package/dist/internal/index.d.ts +2 -3
  164. package/dist/internal/index.d.ts.map +1 -1
  165. package/dist/internal/index.js +2 -2
  166. package/dist/internal/index.js.map +1 -1
  167. package/dist/internal/oauth.d.ts +7 -6
  168. package/dist/internal/oauth.d.ts.map +1 -1
  169. package/dist/internal/oauth.js +3 -3
  170. package/dist/internal/project-link.d.ts +10 -0
  171. package/dist/internal/project-link.d.ts.map +1 -0
  172. package/dist/internal/project-link.js +48 -0
  173. package/dist/internal/project-link.js.map +1 -0
  174. package/dist/providers.json +2 -2
  175. package/dist/server.bundle.mjs +3173 -4404
  176. package/dist/start-local.bundle.mjs +71481 -0
  177. package/dist/templates/README.md.tmpl +10 -11
  178. package/package.json +14 -12
  179. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  180. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  181. package/dist/__tests__/chat.integration.test.js +0 -337
  182. package/dist/__tests__/chat.integration.test.js.map +0 -1
  183. package/dist/__tests__/dev.test.d.ts +0 -2
  184. package/dist/__tests__/dev.test.d.ts.map +0 -1
  185. package/dist/__tests__/dev.test.js +0 -25
  186. package/dist/__tests__/dev.test.js.map +0 -1
  187. package/dist/__tests__/init-memory.test.d.ts +0 -2
  188. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  189. package/dist/__tests__/init-memory.test.js +0 -45
  190. package/dist/__tests__/init-memory.test.js.map +0 -1
  191. package/dist/__tests__/token.test.d.ts +0 -2
  192. package/dist/__tests__/token.test.d.ts.map +0 -1
  193. package/dist/__tests__/token.test.js +0 -52
  194. package/dist/__tests__/token.test.js.map +0 -1
  195. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  196. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  197. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  198. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  199. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  200. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  201. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  202. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  203. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  204. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  205. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  206. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  207. package/dist/commands/apply.d.ts +0 -3
  208. package/dist/commands/apply.d.ts.map +0 -1
  209. package/dist/commands/apply.js +0 -5
  210. package/dist/commands/apply.js.map +0 -1
  211. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  212. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  213. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  214. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  215. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  216. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  217. package/dist/internal/__tests__/api-client.test.js +0 -95
  218. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  219. package/dist/internal/__tests__/context.test.d.ts +0 -2
  220. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  221. package/dist/internal/__tests__/context.test.js +0 -77
  222. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,91 @@
1
+ -- migrate:up
2
+
3
+ -- Normalize watcher feedback to one row per corrected field.
4
+ --
5
+ -- The original `watcher_window_feedback` stored every correction batch as a
6
+ -- single JSONB blob (`corrections`) with one shared `notes` column. That
7
+ -- shape blocks per-field notes, makes "latest correction per field" a JSONB
8
+ -- aggregation, and lets duplicate submissions for the same field accumulate
9
+ -- forever (the prompt summary then injects every historical version).
10
+ --
11
+ -- New table is per-field with explicit mutation kind so structural edits
12
+ -- (remove an array item, append a new one) live alongside value corrections
13
+ -- without overloading the value column. Existing rows are migrated by
14
+ -- expanding the JSONB map into one row per key.
15
+
16
+ CREATE TABLE IF NOT EXISTS public.watcher_window_field_feedback (
17
+ id bigserial PRIMARY KEY,
18
+ window_id integer NOT NULL REFERENCES public.watcher_windows(id) ON DELETE CASCADE,
19
+ watcher_id integer NOT NULL REFERENCES public.watchers(id) ON DELETE CASCADE,
20
+ organization_id text NOT NULL,
21
+ field_path text NOT NULL,
22
+ mutation text NOT NULL DEFAULT 'set'
23
+ CHECK (mutation IN ('set', 'remove', 'add')),
24
+ corrected_value jsonb,
25
+ note text,
26
+ created_by text NOT NULL,
27
+ created_at timestamp with time zone NOT NULL DEFAULT now()
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_wwff_watcher_field_recent
31
+ ON public.watcher_window_field_feedback (watcher_id, field_path, created_at DESC);
32
+ CREATE INDEX IF NOT EXISTS idx_wwff_window
33
+ ON public.watcher_window_field_feedback (window_id);
34
+
35
+ INSERT INTO public.watcher_window_field_feedback (
36
+ window_id, watcher_id, organization_id,
37
+ field_path, mutation, corrected_value, note,
38
+ created_by, created_at
39
+ )
40
+ SELECT
41
+ f.window_id,
42
+ f.watcher_id,
43
+ f.organization_id,
44
+ kv.key AS field_path,
45
+ 'set' AS mutation,
46
+ kv.value AS corrected_value,
47
+ f.notes AS note,
48
+ f.created_by,
49
+ f.created_at
50
+ FROM public.watcher_window_feedback f,
51
+ LATERAL jsonb_each(f.corrections) AS kv;
52
+
53
+ DROP TABLE IF EXISTS public.watcher_window_feedback;
54
+
55
+ -- migrate:down
56
+
57
+ CREATE TABLE IF NOT EXISTS public.watcher_window_feedback (
58
+ id bigserial PRIMARY KEY,
59
+ window_id integer NOT NULL REFERENCES public.watcher_windows(id) ON DELETE CASCADE,
60
+ watcher_id integer NOT NULL REFERENCES public.watchers(id) ON DELETE CASCADE,
61
+ organization_id text NOT NULL,
62
+ corrections jsonb NOT NULL,
63
+ notes text,
64
+ created_by text NOT NULL,
65
+ created_at timestamp with time zone NOT NULL DEFAULT now()
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_wwf_window ON public.watcher_window_feedback(window_id);
69
+ CREATE INDEX IF NOT EXISTS idx_wwf_watcher ON public.watcher_window_feedback(watcher_id);
70
+
71
+ -- Best-effort rollback: structural mutations ('add' / 'remove') don't fit
72
+ -- the old shape and are dropped (WHERE mutation = 'set'). When multiple
73
+ -- contributors edited the same window, MAX(note) and MAX(created_by) pick
74
+ -- one arbitrarily — this is an emergency recovery path, not a clean inverse.
75
+ INSERT INTO public.watcher_window_feedback (
76
+ window_id, watcher_id, organization_id,
77
+ corrections, notes, created_by, created_at
78
+ )
79
+ SELECT
80
+ window_id,
81
+ watcher_id,
82
+ organization_id,
83
+ jsonb_object_agg(field_path, COALESCE(corrected_value, 'null'::jsonb)) AS corrections,
84
+ MAX(note) AS notes,
85
+ MAX(created_by) AS created_by,
86
+ MAX(created_at) AS created_at
87
+ FROM public.watcher_window_field_feedback
88
+ WHERE mutation = 'set'
89
+ GROUP BY window_id, watcher_id, organization_id;
90
+
91
+ DROP TABLE IF EXISTS public.watcher_window_field_feedback;
@@ -0,0 +1,20 @@
1
+ -- migrate:up
2
+
3
+ -- Add diagnostic columns to runs so subprocess failures carry enough context
4
+ -- to diagnose without reading gateway pod logs.
5
+ --
6
+ -- output_tail: redacted tail (~16 KiB) of child stdout+stderr at exit.
7
+ -- exit_code: subprocess exit code, NULL when terminated by IPC error path.
8
+ -- exit_signal: signal name (e.g. SIGKILL) when killed.
9
+ -- exit_reason: categorized — ok | error_message | timeout | oom | crash.
10
+ ALTER TABLE public.runs ADD COLUMN output_tail TEXT NULL;
11
+ ALTER TABLE public.runs ADD COLUMN exit_code INTEGER NULL;
12
+ ALTER TABLE public.runs ADD COLUMN exit_signal TEXT NULL;
13
+ ALTER TABLE public.runs ADD COLUMN exit_reason TEXT NULL;
14
+
15
+ -- migrate:down
16
+
17
+ ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_reason;
18
+ ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_signal;
19
+ ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_code;
20
+ ALTER TABLE public.runs DROP COLUMN IF EXISTS output_tail;
@@ -0,0 +1,46 @@
1
+ -- migrate:up
2
+
3
+ -- Repair-agent plumbing for connector reliability.
4
+ --
5
+ -- When a feed accumulates persistent failures, the worker-completion path
6
+ -- can open a Lobu agent thread for triage. These columns track the
7
+ -- per-feed override, the open thread (if any), the lifetime budget of
8
+ -- repair attempts, the start of the current failure streak, and the
9
+ -- last-posted content hash for append throttling.
10
+ ALTER TABLE public.feeds
11
+ ADD COLUMN repair_agent_id text NULL,
12
+ ADD COLUMN repair_thread_id text NULL,
13
+ ADD COLUMN repair_attempt_count integer NOT NULL DEFAULT 0,
14
+ ADD COLUMN last_repair_at timestamp with time zone NULL,
15
+ ADD COLUMN first_failure_at timestamp with time zone NULL,
16
+ ADD COLUMN last_repair_post_hash text NULL;
17
+
18
+ -- The unique partial index documents intent: a feed has at most one open
19
+ -- repair thread at a time. (`feeds.id` is already PK so the constraint is
20
+ -- redundant for uniqueness; the actual race guard is the conditional
21
+ -- UPDATE on repair_thread_id IS NULL in the worker-completion path.)
22
+ CREATE UNIQUE INDEX feeds_open_repair_thread_uniq
23
+ ON public.feeds (id) WHERE repair_thread_id IS NOT NULL;
24
+
25
+ -- Per-connector default repair agent. When a feed has no explicit
26
+ -- repair_agent_id, the trigger logic falls back to this value.
27
+ ALTER TABLE public.connector_definitions
28
+ ADD COLUMN default_repair_agent_id text NULL;
29
+
30
+ -- Per-org kill switch. When FALSE, no repair threads are opened for any
31
+ -- feed in the org regardless of per-feed configuration.
32
+ ALTER TABLE public.organization
33
+ ADD COLUMN repair_agents_enabled boolean NOT NULL DEFAULT TRUE;
34
+
35
+ -- migrate:down
36
+
37
+ ALTER TABLE public.organization DROP COLUMN IF EXISTS repair_agents_enabled;
38
+ ALTER TABLE public.connector_definitions DROP COLUMN IF EXISTS default_repair_agent_id;
39
+ DROP INDEX IF EXISTS feeds_open_repair_thread_uniq;
40
+ ALTER TABLE public.feeds
41
+ DROP COLUMN IF EXISTS last_repair_post_hash,
42
+ DROP COLUMN IF EXISTS first_failure_at,
43
+ DROP COLUMN IF EXISTS last_repair_at,
44
+ DROP COLUMN IF EXISTS repair_attempt_count,
45
+ DROP COLUMN IF EXISTS repair_thread_id,
46
+ DROP COLUMN IF EXISTS repair_agent_id;
@@ -0,0 +1,101 @@
1
+ -- migrate:up
2
+
3
+ -- Convert entities.entity_type from a text slug to an FK on entity_types(id).
4
+ -- Two motivations folded into one change:
5
+ --
6
+ -- 1. Integrity. Today entity_types renames orphan all referencing entities
7
+ -- (slug-based reference is silent FK with no enforcement). Hard-deletes
8
+ -- bypass the validator entirely. With a real FK, Postgres refuses to
9
+ -- drop a referenced type and renames update for free (the slug becomes
10
+ -- display only — JOIN to entity_types for it).
11
+ --
12
+ -- 2. Cross-org vocabulary. entity_types.id is globally unique (one sequence
13
+ -- across all orgs), so an entity in tenant org A can carry a type defined
14
+ -- in public-catalog org B by FK alone. No additional org_id column on
15
+ -- entities is needed once the slug-based same-org coupling is gone.
16
+ --
17
+ -- Single-prod-DB migration: add nullable column, backfill, fail loudly on
18
+ -- orphans, set NOT NULL, drop the text column. Run manually.
19
+
20
+ -- 1. Add the FK column, nullable for backfill.
21
+ ALTER TABLE public.entities
22
+ ADD COLUMN entity_type_id integer REFERENCES public.entity_types(id);
23
+
24
+ -- 2. Backfill from existing (organization_id, entity_type slug) → entity_types.id.
25
+ -- Prefer live entity_types rows; fall back to soft-deleted ones to preserve
26
+ -- history. Without the ORDER BY, a slug+org pair with both an active and a
27
+ -- soft-deleted row would resolve non-deterministically — entity_types' UNIQUE
28
+ -- index on slug only covers `deleted_at IS NULL` rows, so collisions can exist.
29
+ UPDATE public.entities e
30
+ SET entity_type_id = (
31
+ SELECT et.id
32
+ FROM public.entity_types et
33
+ WHERE et.slug = e.entity_type
34
+ AND et.organization_id = e.organization_id
35
+ ORDER BY (et.deleted_at IS NULL) DESC, et.id DESC
36
+ LIMIT 1
37
+ )
38
+ WHERE e.entity_type_id IS NULL;
39
+
40
+ -- 3. Fail loudly on orphans. If any entities reference a slug with no matching
41
+ -- entity_types row, that's pre-existing data corruption from the slug-based
42
+ -- regime. Surface it; don't paper over.
43
+ DO $$
44
+ DECLARE
45
+ orphan_count integer;
46
+ BEGIN
47
+ SELECT COUNT(*) INTO orphan_count FROM public.entities WHERE entity_type_id IS NULL;
48
+ IF orphan_count > 0 THEN
49
+ RAISE EXCEPTION
50
+ 'entity_type FK migration: % entities have entity_type slugs with no matching entity_types row. Investigate before re-running.',
51
+ orphan_count;
52
+ END IF;
53
+ END $$;
54
+
55
+ -- 4. Tighten the FK column.
56
+ ALTER TABLE public.entities
57
+ ALTER COLUMN entity_type_id SET NOT NULL;
58
+
59
+ -- 5. Index for filter/list queries that previously used entity_type slug.
60
+ CREATE INDEX idx_entities_entity_type_id
61
+ ON public.entities (entity_type_id)
62
+ WHERE deleted_at IS NULL;
63
+
64
+ -- 6. Drop the redundant UNIQUE constraint that referenced entity_type. The
65
+ -- stronger `entities_slug_parent_unique` (UNIQUE on org_id, COALESCE(parent_id,
66
+ -- 0), slug) already enforces slug uniqueness within (org, parent) regardless
67
+ -- of entity type, with NULL-parent collapsing — so this constraint never
68
+ -- caught anything the index didn't already catch. Drop it explicitly rather
69
+ -- than letting DROP COLUMN cascade silently.
70
+ ALTER TABLE public.entities
71
+ DROP CONSTRAINT IF EXISTS entities_organization_id_entity_type_slug_parent_id_key;
72
+
73
+ -- 7. Drop the column comment so DROP COLUMN doesn't carry a stale doc string
74
+ -- if this migration is ever rolled back and re-applied.
75
+ COMMENT ON COLUMN public.entities.entity_type IS NULL;
76
+
77
+ -- 8. Drop the text column. All readers JOIN to entity_types for the slug.
78
+ ALTER TABLE public.entities DROP COLUMN entity_type;
79
+
80
+
81
+ -- migrate:down
82
+
83
+ ALTER TABLE public.entities ADD COLUMN entity_type text;
84
+
85
+ UPDATE public.entities e
86
+ SET entity_type = et.slug
87
+ FROM public.entity_types et
88
+ WHERE et.id = e.entity_type_id;
89
+
90
+ ALTER TABLE public.entities ALTER COLUMN entity_type SET NOT NULL;
91
+
92
+ COMMENT ON COLUMN public.entities.entity_type IS
93
+ 'Type of entity: brand, product (future: location, feature, team)';
94
+
95
+ ALTER TABLE public.entities
96
+ ADD CONSTRAINT entities_organization_id_entity_type_slug_parent_id_key
97
+ UNIQUE (organization_id, entity_type, slug, parent_id);
98
+
99
+ DROP INDEX IF EXISTS public.idx_entities_entity_type_id;
100
+
101
+ ALTER TABLE public.entities DROP COLUMN entity_type_id;
@@ -0,0 +1,104 @@
1
+ -- migrate:up
2
+
3
+ -- DB integrity cleanup pass (transactional half).
4
+ --
5
+ -- This file groups the changes that are safe to run inside dbmate's default
6
+ -- per-migration transaction: small-table tightenings and a UNIQUE swap that
7
+ -- only takes a brief ACCESS EXCLUSIVE lock. Operations that would hold a
8
+ -- long lock on busy tables (events.created_by NOT NULL, runs CHECK swap,
9
+ -- connections UNIQUE) live in the companion `transaction:false` migration
10
+ -- 20260426120001_db_integrity_cleanup_concurrent.sql.
11
+ --
12
+ -- Each tightening validates its precondition first and aborts loudly via
13
+ -- RAISE EXCEPTION if dirty data is present, so a green run on staging is a
14
+ -- strong signal for production.
15
+
16
+ -- 1. Drop the legacy events archive (renamed during the append-only cutover
17
+ -- on 2026-04-06; no application code references it).
18
+
19
+ DROP TABLE IF EXISTS public.events_legacy_pre_append_only_20260406;
20
+
21
+ -- 2. event_classifiers.status / organization_id -> NOT NULL.
22
+ -- `status` already has DEFAULT 'active' but is nullable; backfill any
23
+ -- pre-default rows then enforce. `organization_id` should never be null.
24
+
25
+ UPDATE public.event_classifiers
26
+ SET status = 'active'
27
+ WHERE status IS NULL;
28
+
29
+ DO $$
30
+ DECLARE
31
+ null_org bigint;
32
+ BEGIN
33
+ SELECT count(*) INTO null_org
34
+ FROM public.event_classifiers
35
+ WHERE organization_id IS NULL;
36
+ IF null_org > 0 THEN
37
+ RAISE EXCEPTION
38
+ 'event_classifiers has % row(s) with NULL organization_id; backfill before re-running this migration',
39
+ null_org;
40
+ END IF;
41
+ END$$;
42
+
43
+ ALTER TABLE public.event_classifiers
44
+ ALTER COLUMN status SET NOT NULL,
45
+ ALTER COLUMN organization_id SET NOT NULL;
46
+
47
+ -- 3. watchers.status / organization_id -> NOT NULL.
48
+ -- `status` has DEFAULT 'active'; `organization_id` should never be null.
49
+
50
+ UPDATE public.watchers
51
+ SET status = 'active'
52
+ WHERE status IS NULL;
53
+
54
+ DO $$
55
+ DECLARE
56
+ null_org bigint;
57
+ BEGIN
58
+ SELECT count(*) INTO null_org
59
+ FROM public.watchers
60
+ WHERE organization_id IS NULL;
61
+ IF null_org > 0 THEN
62
+ RAISE EXCEPTION
63
+ 'watchers has % row(s) with NULL organization_id; backfill before re-running this migration',
64
+ null_org;
65
+ END IF;
66
+ END$$;
67
+
68
+ ALTER TABLE public.watchers
69
+ ALTER COLUMN status SET NOT NULL,
70
+ ALTER COLUMN organization_id SET NOT NULL;
71
+
72
+ -- 4. event_classifiers UNIQUE -> NULLS NOT DISTINCT.
73
+ -- The previous (entity_id, watcher_id, slug) UNIQUE silently allowed
74
+ -- duplicates whenever entity_id or watcher_id were NULL because Postgres
75
+ -- treats NULLs as distinct. PG15+ supports NULLS NOT DISTINCT for the
76
+ -- intended "one row per scope+slug" semantic.
77
+ -- The table is small; the brief ACCESS EXCLUSIVE inside this transaction
78
+ -- is acceptable.
79
+
80
+ ALTER TABLE public.event_classifiers
81
+ DROP CONSTRAINT event_classifiers_unique_per_insight;
82
+
83
+ ALTER TABLE public.event_classifiers
84
+ ADD CONSTRAINT event_classifiers_unique_per_insight
85
+ UNIQUE NULLS NOT DISTINCT (entity_id, watcher_id, slug);
86
+
87
+ -- migrate:down
88
+
89
+ ALTER TABLE public.event_classifiers
90
+ DROP CONSTRAINT event_classifiers_unique_per_insight;
91
+
92
+ ALTER TABLE public.event_classifiers
93
+ ADD CONSTRAINT event_classifiers_unique_per_insight
94
+ UNIQUE (entity_id, watcher_id, slug);
95
+
96
+ ALTER TABLE public.watchers
97
+ ALTER COLUMN organization_id DROP NOT NULL,
98
+ ALTER COLUMN status DROP NOT NULL;
99
+
100
+ ALTER TABLE public.event_classifiers
101
+ ALTER COLUMN organization_id DROP NOT NULL,
102
+ ALTER COLUMN status DROP NOT NULL;
103
+
104
+ -- The legacy events table is intentionally not recreated on rollback.
@@ -0,0 +1,187 @@
1
+ -- migrate:up transaction:false
2
+
3
+ -- DB integrity cleanup pass (non-transactional half).
4
+ --
5
+ -- Statements here would hold ACCESS EXCLUSIVE on busy tables for the
6
+ -- duration of a table scan or index build if wrapped in dbmate's default
7
+ -- per-migration transaction. Running with `transaction:false` lets each
8
+ -- statement release its lock on completion so VALIDATE CONSTRAINT only
9
+ -- needs SHARE UPDATE EXCLUSIVE (compatible with concurrent reads/writes)
10
+ -- and CREATE INDEX CONCURRENTLY can do its non-blocking build.
11
+ --
12
+ -- All steps are written to be idempotent: re-running after a partial
13
+ -- failure converges on the desired state. Validation aborts (RAISE
14
+ -- EXCEPTION) leave the migration NOT applied so dbmate retries cleanly.
15
+ --
16
+ -- Set a short lock_timeout so any contention surfaces as a fast failure
17
+ -- instead of a stuck deploy.
18
+ SET lock_timeout = '5s';
19
+
20
+ -- 1. events.created_by -> NOT NULL.
21
+ -- ADD CHECK NOT VALID adds the catalog row under a brief ACCESS
22
+ -- EXCLUSIVE without scanning the table. VALIDATE then takes only
23
+ -- SHARE UPDATE EXCLUSIVE so concurrent reads/writes are unaffected.
24
+ -- Once validated, SET NOT NULL is metadata-only because Postgres uses
25
+ -- the validated CHECK as proof there are no NULLs (PG12+).
26
+
27
+ DO $$
28
+ BEGIN
29
+ IF NOT EXISTS (
30
+ SELECT 1
31
+ FROM pg_constraint
32
+ WHERE conrelid = 'public.events'::regclass
33
+ AND conname = 'events_created_by_not_null'
34
+ ) THEN
35
+ ALTER TABLE public.events
36
+ ADD CONSTRAINT events_created_by_not_null
37
+ CHECK (created_by IS NOT NULL) NOT VALID;
38
+ END IF;
39
+ END$$;
40
+
41
+ -- Backfill historical NULLs with the existing 'system' sentinel before
42
+ -- VALIDATE. Pre-`created_by` events end up here.
43
+ --
44
+ -- The session's lock_timeout is 5s (set at the top of this migration)
45
+ -- which is appropriate for DDL but too aggressive for a row-level
46
+ -- backfill running concurrently with live inserts: the deploying
47
+ -- gateway and the live old pod can hold per-row locks briefly, and
48
+ -- our UPDATE waits on each one. Bump locally for the UPDATE and the
49
+ -- VALIDATE that follows, then restore.
50
+ SET lock_timeout = 0;
51
+ UPDATE public.events SET created_by = 'system' WHERE created_by IS NULL;
52
+
53
+ ALTER TABLE public.events
54
+ VALIDATE CONSTRAINT events_created_by_not_null;
55
+
56
+ ALTER TABLE public.events
57
+ ALTER COLUMN created_by SET NOT NULL;
58
+
59
+ ALTER TABLE public.events
60
+ DROP CONSTRAINT IF EXISTS events_created_by_not_null;
61
+
62
+ SET lock_timeout = '5s';
63
+
64
+ -- 2. Prune historical run_type values from the runs CHECK.
65
+ -- 'embed_backfill' is still in active use and stays. 'insight' and
66
+ -- 'code' are remnants of earlier orchestration models with no
67
+ -- production references; abort if any rows still carry them.
68
+ -- Use the NOT VALID + VALIDATE pattern so the swap doesn't hold
69
+ -- ACCESS EXCLUSIVE on runs across the table scan.
70
+
71
+ DO $$
72
+ DECLARE
73
+ historical bigint;
74
+ BEGIN
75
+ SELECT count(*) INTO historical
76
+ FROM public.runs
77
+ WHERE run_type IN ('insight', 'code');
78
+ IF historical > 0 THEN
79
+ RAISE EXCEPTION
80
+ 'runs has % row(s) with deprecated run_type (insight/code); migrate them before re-running',
81
+ historical;
82
+ END IF;
83
+ END$$;
84
+
85
+ DO $$
86
+ BEGIN
87
+ IF NOT EXISTS (
88
+ SELECT 1
89
+ FROM pg_constraint
90
+ WHERE conrelid = 'public.runs'::regclass
91
+ AND conname = 'runs_run_type_check_v2'
92
+ ) THEN
93
+ ALTER TABLE public.runs
94
+ ADD CONSTRAINT runs_run_type_check_v2
95
+ CHECK (run_type = ANY (ARRAY[
96
+ 'sync'::text,
97
+ 'action'::text,
98
+ 'embed_backfill'::text,
99
+ 'watcher'::text,
100
+ 'auth'::text
101
+ ])) NOT VALID;
102
+ END IF;
103
+ END$$;
104
+
105
+ ALTER TABLE public.runs
106
+ VALIDATE CONSTRAINT runs_run_type_check_v2;
107
+
108
+ ALTER TABLE public.runs
109
+ DROP CONSTRAINT IF EXISTS runs_run_type_check;
110
+
111
+ ALTER TABLE public.runs
112
+ RENAME CONSTRAINT runs_run_type_check_v2 TO runs_run_type_check;
113
+
114
+ -- 3. Connections natural-key UNIQUE among live, authenticated rows.
115
+ -- A reconnect should refresh credentials in place, not insert a new
116
+ -- row. Partial index limits scope to non-deleted rows with an
117
+ -- account_id so soft-deletes and unauthenticated stubs don't block
118
+ -- re-creation. CONCURRENTLY avoids blocking writes during the build.
119
+
120
+ DO $$
121
+ DECLARE
122
+ dup_groups bigint;
123
+ BEGIN
124
+ SELECT count(*) INTO dup_groups
125
+ FROM (
126
+ SELECT 1
127
+ FROM public.connections
128
+ WHERE deleted_at IS NULL
129
+ AND account_id IS NOT NULL
130
+ GROUP BY organization_id, connector_key, account_id
131
+ HAVING count(*) > 1
132
+ ) t;
133
+ IF dup_groups > 0 THEN
134
+ RAISE EXCEPTION
135
+ 'connections has % duplicate (organization_id, connector_key, account_id) group(s) among live rows; dedupe before re-running',
136
+ dup_groups;
137
+ END IF;
138
+ END$$;
139
+
140
+ -- A previous failed run can leave an INVALID index behind. `IF NOT EXISTS`
141
+ -- alone would skip creation in that case, leaving uniqueness unenforced.
142
+ -- Drop first (no-op when the index doesn't exist) so the create always
143
+ -- produces a valid index.
144
+ --
145
+ -- Originally CONCURRENTLY (which is why this migration is in the
146
+ -- transaction:false half) but dbmate's transaction handling against
147
+ -- the pq driver still presents these as in-transaction to Postgres,
148
+ -- failing with "DROP INDEX CONCURRENTLY cannot run inside a
149
+ -- transaction block". The connections table only has a handful of
150
+ -- rows matching the partial-index predicate (≈2 in prod), so the
151
+ -- ACCESS EXCLUSIVE held during a non-concurrent build is sub-second.
152
+ DROP INDEX IF EXISTS public.idx_connections_org_connector_account_live;
153
+
154
+ CREATE UNIQUE INDEX idx_connections_org_connector_account_live
155
+ ON public.connections (organization_id, connector_key, account_id)
156
+ WHERE deleted_at IS NULL AND account_id IS NOT NULL;
157
+
158
+ -- migrate:down transaction:false
159
+
160
+ DROP INDEX CONCURRENTLY IF EXISTS public.idx_connections_org_connector_account_live;
161
+
162
+ DO $$
163
+ BEGIN
164
+ IF EXISTS (
165
+ SELECT 1
166
+ FROM pg_constraint
167
+ WHERE conrelid = 'public.runs'::regclass
168
+ AND conname = 'runs_run_type_check'
169
+ ) THEN
170
+ ALTER TABLE public.runs DROP CONSTRAINT runs_run_type_check;
171
+ END IF;
172
+ END$$;
173
+
174
+ ALTER TABLE public.runs
175
+ ADD CONSTRAINT runs_run_type_check
176
+ CHECK (run_type = ANY (ARRAY[
177
+ 'sync'::text,
178
+ 'action'::text,
179
+ 'code'::text,
180
+ 'insight'::text,
181
+ 'embed_backfill'::text,
182
+ 'watcher'::text,
183
+ 'auth'::text
184
+ ]));
185
+
186
+ ALTER TABLE public.events
187
+ ALTER COLUMN created_by DROP NOT NULL;
@@ -0,0 +1,74 @@
1
+ -- migrate:up transaction:false
2
+
3
+ -- `events.created_by` represents the Lobu user that explicitly saved or
4
+ -- authored an event. Connector/scheduled/system-produced events have no human
5
+ -- saver; their provenance is represented by connector_key, connection_id,
6
+ -- feed_id, run_id, and client_id. Keep user attribution nullable and enforce
7
+ -- that any non-null value is a real user id instead of a sentinel string.
8
+ SET lock_timeout = '5s';
9
+
10
+ ALTER TABLE public.events
11
+ ALTER COLUMN created_by DROP NOT NULL;
12
+
13
+ -- During a rolling deploy, old application pods may still write the historical
14
+ -- 'system'/'api' sentinel actors. Normalize only those known sentinels to NULL
15
+ -- before the FK sees them. Unknown non-user values should still be rejected.
16
+ CREATE OR REPLACE FUNCTION public.normalize_event_created_by()
17
+ RETURNS trigger
18
+ LANGUAGE plpgsql
19
+ AS $$
20
+ BEGIN
21
+ IF NEW.created_by IN ('system', 'api') AND NOT EXISTS (
22
+ SELECT 1 FROM public."user" u WHERE u.id = NEW.created_by
23
+ ) THEN
24
+ NEW.created_by := NULL;
25
+ END IF;
26
+ RETURN NEW;
27
+ END;
28
+ $$;
29
+
30
+ DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events;
31
+ CREATE TRIGGER normalize_event_created_by
32
+ BEFORE INSERT OR UPDATE OF created_by ON public.events
33
+ FOR EACH ROW
34
+ EXECUTE FUNCTION public.normalize_event_created_by();
35
+
36
+ -- The previous integrity migration backfilled NULL event actors to 'system'.
37
+ -- Convert those sentinel values back to NULL. This uses idx_events_created_by.
38
+ SET lock_timeout = 0;
39
+ UPDATE public.events e
40
+ SET created_by = NULL
41
+ WHERE e.created_by IN ('system', 'api')
42
+ AND NOT EXISTS (
43
+ SELECT 1 FROM public."user" u WHERE u.id = e.created_by
44
+ );
45
+ SET lock_timeout = '5s';
46
+
47
+ ALTER TABLE public.events
48
+ ADD CONSTRAINT events_created_by_fkey
49
+ FOREIGN KEY (created_by)
50
+ REFERENCES public."user"(id)
51
+ ON DELETE SET NULL
52
+ NOT VALID;
53
+
54
+ SET lock_timeout = 0;
55
+ ALTER TABLE public.events
56
+ VALIDATE CONSTRAINT events_created_by_fkey;
57
+ SET lock_timeout = '5s';
58
+
59
+ -- migrate:down transaction:false
60
+
61
+ ALTER TABLE public.events
62
+ DROP CONSTRAINT IF EXISTS events_created_by_fkey;
63
+
64
+ DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events;
65
+ DROP FUNCTION IF EXISTS public.normalize_event_created_by();
66
+
67
+ SET lock_timeout = 0;
68
+ UPDATE public.events
69
+ SET created_by = 'system'
70
+ WHERE created_by IS NULL;
71
+ SET lock_timeout = '5s';
72
+
73
+ ALTER TABLE public.events
74
+ ALTER COLUMN created_by SET NOT NULL;