@lobu/cli 7.0.0 → 7.2.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 (137) hide show
  1. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  2. package/dist/commands/_lib/apply/apply-cmd.js +160 -12
  3. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  4. package/dist/commands/_lib/apply/client.d.ts +106 -0
  5. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  6. package/dist/commands/_lib/apply/client.js +163 -2
  7. package/dist/commands/_lib/apply/client.js.map +1 -1
  8. package/dist/commands/_lib/apply/desired-state.d.ts +53 -0
  9. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  10. package/dist/commands/_lib/apply/desired-state.js +182 -5
  11. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  12. package/dist/commands/_lib/apply/diff.d.ts +12 -1
  13. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  14. package/dist/commands/_lib/apply/diff.js +106 -7
  15. package/dist/commands/_lib/apply/diff.js.map +1 -1
  16. package/dist/commands/_lib/connector-loader.d.ts +3 -0
  17. package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
  18. package/dist/commands/_lib/connector-loader.js +129 -0
  19. package/dist/commands/_lib/connector-loader.js.map +1 -0
  20. package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
  21. package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
  22. package/dist/commands/_lib/connector-run-cmd.js +351 -0
  23. package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
  24. package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
  25. package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
  26. package/dist/commands/_lib/export/export-cmd.js +329 -0
  27. package/dist/commands/_lib/export/export-cmd.js.map +1 -0
  28. package/dist/commands/agent.d.ts.map +1 -1
  29. package/dist/commands/agent.js +11 -14
  30. package/dist/commands/agent.js.map +1 -1
  31. package/dist/commands/chat.d.ts.map +1 -1
  32. package/dist/commands/chat.js +19 -5
  33. package/dist/commands/chat.js.map +1 -1
  34. package/dist/commands/connector.d.ts +3 -0
  35. package/dist/commands/connector.d.ts.map +1 -0
  36. package/dist/commands/connector.js +5 -0
  37. package/dist/commands/connector.js.map +1 -0
  38. package/dist/commands/context.d.ts +7 -0
  39. package/dist/commands/context.d.ts.map +1 -1
  40. package/dist/commands/context.js +19 -2
  41. package/dist/commands/context.js.map +1 -1
  42. package/dist/commands/dev.d.ts +15 -0
  43. package/dist/commands/dev.d.ts.map +1 -1
  44. package/dist/commands/dev.js +156 -4
  45. package/dist/commands/dev.js.map +1 -1
  46. package/dist/commands/doctor.d.ts.map +1 -1
  47. package/dist/commands/doctor.js +2 -3
  48. package/dist/commands/doctor.js.map +1 -1
  49. package/dist/commands/eval.d.ts.map +1 -1
  50. package/dist/commands/eval.js +12 -13
  51. package/dist/commands/eval.js.map +1 -1
  52. package/dist/commands/init.d.ts.map +1 -1
  53. package/dist/commands/init.js +5 -1
  54. package/dist/commands/init.js.map +1 -1
  55. package/dist/commands/login.d.ts.map +1 -1
  56. package/dist/commands/login.js +22 -16
  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 +15 -144
  60. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  61. package/dist/commands/token.d.ts.map +1 -1
  62. package/dist/commands/token.js +1 -4
  63. package/dist/commands/token.js.map +1 -1
  64. package/dist/commands/validate.d.ts.map +1 -1
  65. package/dist/commands/validate.js +4 -13
  66. package/dist/commands/validate.js.map +1 -1
  67. package/dist/config/loader.js +2 -2
  68. package/dist/config/loader.js.map +1 -1
  69. package/dist/connectors/README.md +0 -1
  70. package/dist/connectors/apple_photos.ts +178 -0
  71. package/dist/connectors/browser-scraper-utils.ts +76 -0
  72. package/dist/connectors/chrome.ts +351 -0
  73. package/dist/connectors/chrome_bookmarks.ts +79 -0
  74. package/dist/connectors/chrome_downloads.ts +80 -0
  75. package/dist/connectors/chrome_history.ts +80 -0
  76. package/dist/connectors/github.ts +1 -0
  77. package/dist/connectors/google_calendar.ts +14 -2
  78. package/dist/connectors/google_play.ts +22 -2
  79. package/dist/connectors/hackernews.ts +37 -2
  80. package/dist/connectors/index.ts +15 -1
  81. package/dist/connectors/reddit.ts +1 -0
  82. package/dist/connectors/revolut.ts +10 -13
  83. package/dist/connectors/rss.ts +33 -8
  84. package/dist/connectors/trustpilot.ts +31 -20
  85. package/dist/connectors/website.ts +7 -68
  86. package/dist/connectors/whatsapp.ts +12 -21
  87. package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
  88. package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
  89. package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
  90. package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
  91. package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
  92. package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
  93. package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
  94. package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
  95. package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
  96. package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
  97. package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
  98. package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
  99. package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
  100. package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
  101. package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
  102. package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
  103. package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
  104. package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
  105. package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
  106. package/dist/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql +36 -0
  107. package/dist/db/migrations/20260518040000_agent_transcript_snapshot.sql +54 -0
  108. package/dist/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql +36 -0
  109. package/dist/db/migrations/20260518060000_revert_runs_denormalize.sql +29 -0
  110. package/dist/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql +33 -0
  111. package/dist/eval/client.d.ts.map +1 -1
  112. package/dist/eval/client.js +11 -0
  113. package/dist/eval/client.js.map +1 -1
  114. package/dist/eval/grader.js +2 -1
  115. package/dist/eval/grader.js.map +1 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +84 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/internal/context.d.ts +13 -1
  120. package/dist/internal/context.d.ts.map +1 -1
  121. package/dist/internal/context.js +83 -8
  122. package/dist/internal/context.js.map +1 -1
  123. package/dist/internal/credentials.d.ts +5 -0
  124. package/dist/internal/credentials.d.ts.map +1 -1
  125. package/dist/internal/credentials.js +75 -1
  126. package/dist/internal/credentials.js.map +1 -1
  127. package/dist/internal/index.d.ts +2 -2
  128. package/dist/internal/index.d.ts.map +1 -1
  129. package/dist/internal/index.js +2 -2
  130. package/dist/internal/index.js.map +1 -1
  131. package/dist/internal/local-env.d.ts.map +1 -1
  132. package/dist/internal/local-env.js +9 -2
  133. package/dist/internal/local-env.js.map +1 -1
  134. package/dist/server.bundle.mjs +7085 -2832
  135. package/dist/start-local.bundle.mjs +8269 -3656
  136. package/package.json +7 -5
  137. package/dist/connectors/google_photos.ts +0 -776
@@ -0,0 +1,125 @@
1
+ -- migrate:up
2
+ -- Phase C: swap `agents` PK from globally-unique `id` to per-org composite
3
+ -- `(organization_id, id)`. The application has always treated agents as
4
+ -- org-scoped (every read/list filters by organization_id) but the global PK
5
+ -- silently blocked two orgs from sharing an agent ID — for example, a stale
6
+ -- `food-ordering` in one org would prevent `food-ordering` in another.
7
+ --
8
+ -- Phase A (20260515120000) added the org column + composite indexes on the
9
+ -- 5 FK-holding child tables and backfilled values from agents. Phase B (the
10
+ -- application-code refactor in this PR) plumbs `organization_id` through every
11
+ -- INSERT/UPDATE/DELETE/SELECT touching these tables. This migration is the
12
+ -- final structural swap: it drops the single-column PK + FKs, adds the
13
+ -- composite PK + FKs, and widens the per-(agent,kind,pattern) uniques on
14
+ -- agent_users / agent_grants / grants with organization_id.
15
+
16
+ -- ── 0. Set NOT NULL on the columns Phase A backfilled.
17
+ -- Backfill defensively in case any rows snuck in NULL (e.g. embedded PGlite
18
+ -- installs that bypassed the dbmate runner during a partial state).
19
+ UPDATE public.agent_grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
20
+ UPDATE public.agent_connections c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
21
+ UPDATE public.agent_users c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
22
+ UPDATE public.agent_channel_bindings c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
23
+ UPDATE public.grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
24
+
25
+ -- Drop any orphan rows (agent_id with no matching agents row). Backfill
26
+ -- can't recover these.
27
+ DELETE FROM public.agent_grants WHERE organization_id IS NULL;
28
+ DELETE FROM public.agent_connections WHERE organization_id IS NULL;
29
+ DELETE FROM public.agent_users WHERE organization_id IS NULL;
30
+ DELETE FROM public.agent_channel_bindings WHERE organization_id IS NULL;
31
+ DELETE FROM public.grants WHERE organization_id IS NULL;
32
+
33
+ ALTER TABLE public.agent_grants ALTER COLUMN organization_id SET NOT NULL;
34
+ ALTER TABLE public.agent_connections ALTER COLUMN organization_id SET NOT NULL;
35
+ ALTER TABLE public.agent_users ALTER COLUMN organization_id SET NOT NULL;
36
+ ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id SET NOT NULL;
37
+ ALTER TABLE public.grants ALTER COLUMN organization_id SET NOT NULL;
38
+
39
+ -- ── 1. Drop the 6 single-column FKs into agents(id).
40
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_fkey;
41
+ ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_agent_id_fkey;
42
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_agent_id_fkey;
43
+ ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_agent_id_fkey;
44
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_agent_id_fkey;
45
+ ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_agent_fkey;
46
+
47
+ -- ── 2. Drop the unique/PK constraints on child tables that scope to bare agent_id.
48
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_pattern_key;
49
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
50
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;
51
+
52
+ -- ── 3. Swap the PK on agents from (id) to (organization_id, id).
53
+ ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
54
+ ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id);
55
+
56
+ -- ── 4. Re-add per-org-scoped uniques on the child tables.
57
+ ALTER TABLE public.agent_grants
58
+ ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern);
59
+ ALTER TABLE public.agent_users
60
+ ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id);
61
+ ALTER TABLE public.grants
62
+ ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern);
63
+
64
+ -- ── 5. Re-add composite FKs (organization_id, agent_id) → agents(organization_id, id).
65
+ ALTER TABLE public.agent_grants
66
+ ADD CONSTRAINT agent_grants_org_agent_fkey
67
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
68
+ ALTER TABLE public.agent_connections
69
+ ADD CONSTRAINT agent_connections_org_agent_fkey
70
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
71
+ ALTER TABLE public.agent_users
72
+ ADD CONSTRAINT agent_users_org_agent_fkey
73
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
74
+ ALTER TABLE public.agent_channel_bindings
75
+ ADD CONSTRAINT agent_channel_bindings_org_agent_fkey
76
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
77
+ ALTER TABLE public.grants
78
+ ADD CONSTRAINT grants_org_agent_fkey
79
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
80
+ ALTER TABLE public.scheduled_jobs
81
+ ADD CONSTRAINT scheduled_jobs_org_agent_fkey
82
+ FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
83
+
84
+ -- migrate:down
85
+ -- Reverse the swap. NOTE: this WILL FAIL if two orgs ended up sharing an
86
+ -- agent ID after this migration shipped (the previous PK on (id) requires
87
+ -- global uniqueness). That's by design — this migration's whole purpose is
88
+ -- to allow per-org agent IDs that the old PK forbids.
89
+
90
+ ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_org_agent_fkey;
91
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_org_agent_fkey;
92
+ ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_org_agent_fkey;
93
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_org_agent_fkey;
94
+ ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_org_agent_fkey;
95
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_fkey;
96
+
97
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;
98
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
99
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_pattern_key;
100
+
101
+ ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
102
+ ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (id);
103
+
104
+ ALTER TABLE public.agent_grants ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern);
105
+ ALTER TABLE public.agent_users ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id);
106
+ ALTER TABLE public.grants ADD CONSTRAINT grants_pkey PRIMARY KEY (agent_id, kind, pattern);
107
+
108
+ ALTER TABLE public.agent_grants
109
+ ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
110
+ ALTER TABLE public.agent_connections
111
+ ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
112
+ ALTER TABLE public.agent_users
113
+ ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
114
+ ALTER TABLE public.agent_channel_bindings
115
+ ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
116
+ ALTER TABLE public.grants
117
+ ADD CONSTRAINT grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
118
+ ALTER TABLE public.scheduled_jobs
119
+ ADD CONSTRAINT scheduled_jobs_agent_fkey FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE;
120
+
121
+ ALTER TABLE public.grants ALTER COLUMN organization_id DROP NOT NULL;
122
+ ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id DROP NOT NULL;
123
+ ALTER TABLE public.agent_users ALTER COLUMN organization_id DROP NOT NULL;
124
+ ALTER TABLE public.agent_connections ALTER COLUMN organization_id DROP NOT NULL;
125
+ ALTER TABLE public.agent_grants ALTER COLUMN organization_id DROP NOT NULL;
@@ -0,0 +1,134 @@
1
+ -- migrate:up
2
+
3
+ -- Materialize the fulltext search vector as a STORED column.
4
+ --
5
+ -- Why a real column instead of the previous expression-indexed `to_tsvector(payload_text)`:
6
+ -- 1. It includes both `title` (weight A) and `payload_text` (weight B) — the
7
+ -- same shape buildSearchDocumentExpr() in content-search.ts uses for
8
+ -- ranking. Retrieval (@@) and ts_rank_cd now read the same vector, so
9
+ -- title-only hits surface correctly and ranking doesn't recompute the
10
+ -- vector per matched row at query time.
11
+ -- 2. Planner-stable: `search_tsv @@ to_tsquery(...)` is a plain column
12
+ -- reference — no expression-shape matching, no aliasing risk where the
13
+ -- GIN gets skipped because the WHERE expression isn't byte-identical to
14
+ -- the indexed expression.
15
+ -- 3. The new GIN strictly subsumes the old payload-only one (same lexemes
16
+ -- plus title's), so we drop the old index and recover its write
17
+ -- amplification on every events insert.
18
+ --
19
+ -- Operational note: ADD COLUMN ... GENERATED STORED rewrites the events
20
+ -- table under ACCESS EXCLUSIVE. On a 1M-row table expect on the order of a
21
+ -- minute; run during a quiet window. CONCURRENTLY does not apply to ADD
22
+ -- COLUMN; it only applies to CREATE INDEX, which is a separate statement
23
+ -- below.
24
+
25
+ ALTER TABLE public.events
26
+ ADD COLUMN search_tsv tsvector GENERATED ALWAYS AS (
27
+ setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
28
+ setweight(to_tsvector('english', COALESCE(payload_text, '')), 'B')
29
+ ) STORED;
30
+
31
+ CREATE INDEX idx_events_search_tsv ON public.events USING gin (search_tsv);
32
+
33
+ DROP INDEX IF EXISTS public.idx_events_fulltext;
34
+
35
+ -- Recreate the current_event_records view so it exposes search_tsv to
36
+ -- callers that go through the supersession filter (the view materializes
37
+ -- its column list at create time, so `e.*`-style auto-pickup doesn't
38
+ -- apply). buildSearchDocumentExpr() reads `f.search_tsv` / `fi.search_tsv`
39
+ -- where f and fi are this view's aliases, so without this the view-backed
40
+ -- queries fail with "column ... does not exist".
41
+
42
+ DROP VIEW IF EXISTS public.current_event_records;
43
+ CREATE VIEW public.current_event_records AS
44
+ SELECT e.id,
45
+ e.organization_id,
46
+ e.entity_ids,
47
+ e.origin_id,
48
+ e.title,
49
+ e.payload_type,
50
+ e.payload_text,
51
+ e.payload_data,
52
+ e.payload_template,
53
+ e.attachments,
54
+ e.metadata,
55
+ e.score,
56
+ emb.embedding,
57
+ e.author_name,
58
+ e.source_url,
59
+ e.occurred_at,
60
+ e.created_at,
61
+ e.origin_parent_id,
62
+ COALESCE(length(e.payload_text), 0) AS content_length,
63
+ e.search_tsv,
64
+ e.origin_type,
65
+ e.connector_key,
66
+ e.connection_id,
67
+ e.feed_key,
68
+ e.feed_id,
69
+ e.run_id,
70
+ e.semantic_type,
71
+ e.client_id,
72
+ e.created_by,
73
+ e.interaction_type,
74
+ e.interaction_status,
75
+ e.interaction_input_schema,
76
+ e.interaction_input,
77
+ e.interaction_output,
78
+ e.interaction_error,
79
+ e.supersedes_event_id
80
+ FROM (public.events e
81
+ LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id)))
82
+ WHERE (NOT (EXISTS ( SELECT 1
83
+ FROM public.events newer
84
+ WHERE (newer.supersedes_event_id = e.id))));
85
+
86
+ -- migrate:down
87
+
88
+ DROP VIEW IF EXISTS public.current_event_records;
89
+ CREATE VIEW public.current_event_records AS
90
+ SELECT e.id,
91
+ e.organization_id,
92
+ e.entity_ids,
93
+ e.origin_id,
94
+ e.title,
95
+ e.payload_type,
96
+ e.payload_text,
97
+ e.payload_data,
98
+ e.payload_template,
99
+ e.attachments,
100
+ e.metadata,
101
+ e.score,
102
+ emb.embedding,
103
+ e.author_name,
104
+ e.source_url,
105
+ e.occurred_at,
106
+ e.created_at,
107
+ e.origin_parent_id,
108
+ COALESCE(length(e.payload_text), 0) AS content_length,
109
+ e.origin_type,
110
+ e.connector_key,
111
+ e.connection_id,
112
+ e.feed_key,
113
+ e.feed_id,
114
+ e.run_id,
115
+ e.semantic_type,
116
+ e.client_id,
117
+ e.created_by,
118
+ e.interaction_type,
119
+ e.interaction_status,
120
+ e.interaction_input_schema,
121
+ e.interaction_input,
122
+ e.interaction_output,
123
+ e.interaction_error,
124
+ e.supersedes_event_id
125
+ FROM (public.events e
126
+ LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id)))
127
+ WHERE (NOT (EXISTS ( SELECT 1
128
+ FROM public.events newer
129
+ WHERE (newer.supersedes_event_id = e.id))));
130
+
131
+ CREATE INDEX idx_events_fulltext ON public.events
132
+ USING gin (to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text)));
133
+ DROP INDEX IF EXISTS public.idx_events_search_tsv;
134
+ ALTER TABLE public.events DROP COLUMN search_tsv;
@@ -0,0 +1,25 @@
1
+ -- migrate:up
2
+
3
+ -- Partial index for the dashboard's 14-day lifecycle-cumulative-stats query
4
+ -- (lifecycleCumulativeStatsSql in packages/owletto/src/lib/api/metric-series.ts).
5
+ --
6
+ -- The query reads events older than `now() - interval '14 days'` and
7
+ -- aggregates by date_trunc('day', created_at), with every COUNT FILTER
8
+ -- already gated on semantic_type='change' AND metadata->>'category'='lifecycle'.
9
+ -- A partial index whose predicate matches that gate prunes the scan from
10
+ -- "all events in the last 14 days for the org" to "just the lifecycle
11
+ -- changes" — typically a small slice of total events. The aggregation
12
+ -- itself is cheap once the row set is narrow.
13
+ --
14
+ -- Why not a materialized view: at current scale the bottleneck is the row
15
+ -- scan, not the per-bucket COUNT FILTER work. A partial index removes the
16
+ -- scan cost without introducing a refresh job or staleness. Revisit if the
17
+ -- post-prune row count grows into the millions.
18
+
19
+ CREATE INDEX idx_events_lifecycle_changes
20
+ ON public.events (organization_id, created_at)
21
+ WHERE semantic_type = 'change' AND metadata->>'category' = 'lifecycle';
22
+
23
+ -- migrate:down
24
+
25
+ DROP INDEX IF EXISTS public.idx_events_lifecycle_changes;
@@ -0,0 +1,49 @@
1
+ -- migrate:up
2
+
3
+ -- Drop 4 indexes that pg_stat_user_indexes reported `idx_scan = 0` after 28h
4
+ -- of prod uptime AND are not referenced from any active code path.
5
+ --
6
+ -- A larger set (4 more, ~5 GB combined) was originally bundled here but
7
+ -- review caught they're not actually unused — they're dormant. The three
8
+ -- big search indexes (`idx_events_embedding`, `idx_events_raw_content_trgm`,
9
+ -- `idx_events_search_tsv`) are explicitly used by the ANN/fulltext/trigram
10
+ -- branches of `approximate_candidate_search` in
11
+ -- `packages/server/src/utils/content-search.ts:1707-1733`. The `search()`
12
+ -- agent tool path threads through there; prod just hasn't called it in
13
+ -- 28h, but a single user-initiated search would now time out at 6s and
14
+ -- return empty results without those indexes (`content-search.ts:1850-1863`).
15
+ -- Similarly `idx_events_run_id` backs the "view in memory" filter
16
+ -- (`content-query-filters.ts:197-201`); rare, but a real path.
17
+ --
18
+ -- Keep those four until either (a) the dormant features are removed in
19
+ -- code, or (b) measured prod traffic confirms they're abandoned.
20
+ --
21
+ -- What remains is small but still real write amplification: each kept
22
+ -- INSERT into events updates these btrees. Combined size ~66 MB —
23
+ -- modest reclaim, but zero downside since the underlying queries don't
24
+ -- exist anywhere in the codebase today (verified by grep).
25
+ --
26
+ -- Plain `DROP INDEX` (not CONCURRENTLY) is used because dbmate's
27
+ -- `transaction:false` directive doesn't actually exit the transaction
28
+ -- block against the `pq` driver — see the comment in
29
+ -- 20260426130001_db_integrity_cleanup_concurrent.sql. These 4 indexes
30
+ -- are all small btrees so the ACCESS EXCLUSIVE on `events` during the
31
+ -- drop is sub-second; no operator runbook needed.
32
+
33
+ DROP INDEX IF EXISTS public.idx_events_entity_ids_occurred_at;
34
+ DROP INDEX IF EXISTS public.idx_events_origin_parent_id;
35
+ DROP INDEX IF EXISTS public.idx_events_thread_lookup;
36
+ DROP INDEX IF EXISTS public.idx_events_type;
37
+
38
+ -- migrate:down
39
+
40
+ CREATE INDEX IF NOT EXISTS idx_events_entity_ids_occurred_at
41
+ ON public.events USING btree ((entity_ids[1]), occurred_at DESC, id DESC)
42
+ WHERE ((entity_ids IS NOT NULL) AND (entity_ids <> '{}'::bigint[]));
43
+ CREATE INDEX IF NOT EXISTS idx_events_origin_parent_id
44
+ ON public.events USING btree (origin_parent_id);
45
+ CREATE INDEX IF NOT EXISTS idx_events_thread_lookup
46
+ ON public.events USING btree (origin_parent_id, occurred_at)
47
+ WHERE (origin_parent_id IS NOT NULL);
48
+ CREATE INDEX IF NOT EXISTS idx_events_type
49
+ ON public.events USING btree (origin_type) WHERE (origin_type IS NOT NULL);
@@ -0,0 +1,56 @@
1
+ -- migrate:up
2
+
3
+ -- Soft-delete feeds whose (connector_key, organization_id) has no active
4
+ -- connector_definition row.
5
+ --
6
+ -- Why: the 2026-05-16 audit found feeds 117-155 (and others) referencing
7
+ -- connector_key='website' in orgs that have no active definition for it
8
+ -- (only one org has `website` active; one definition is archived). Every
9
+ -- CheckDueFeeds tick (every minute) tried to materialize a sync run for
10
+ -- these feeds and threw "No active connector definition found for X." —
11
+ -- producing ~380 error logs / minute that masked real signal in stdout.
12
+ --
13
+ -- The app-side code path now warns + skips (no throw) for the same case
14
+ -- so future orphans don't spam logs either. This migration is the one-time
15
+ -- cleanup of the existing data.
16
+ --
17
+ -- Conservative criteria — match exactly the set CheckDueFeeds processes
18
+ -- (so we only soft-delete feeds that actually produce the error stream).
19
+ -- - feed has no pinned_version (= would have looked up connector_definitions)
20
+ -- - feed.deleted_at IS NULL (still considered active)
21
+ -- - feed.status = 'active' (CheckDueFeeds filters on this — see
22
+ -- packages/server/src/scheduled/check-due-feeds.ts:36-43)
23
+ -- - connection.deleted_at IS NULL AND connection.status = 'active' (same)
24
+ -- - NO active connector_definition exists for that (key, organization) pair
25
+ --
26
+ -- Feeds in paused / pending_auth / error / revoked states are left alone
27
+ -- — operators may be mid-recovery on them and they don't contribute to
28
+ -- the error spam (CheckDueFeeds skips them anyway).
29
+ --
30
+ -- The same feed remains recoverable: clearing `deleted_at` + reinstalling
31
+ -- the connector definition for the org restores it.
32
+
33
+ UPDATE public.feeds f
34
+ SET deleted_at = now()
35
+ FROM public.connections c
36
+ WHERE f.connection_id = c.id
37
+ AND f.deleted_at IS NULL
38
+ AND f.pinned_version IS NULL
39
+ AND f.status = 'active'
40
+ AND c.deleted_at IS NULL
41
+ AND c.status = 'active'
42
+ AND NOT EXISTS (
43
+ SELECT 1
44
+ FROM public.connector_definitions cd
45
+ WHERE cd.key = c.connector_key
46
+ AND cd.organization_id = f.organization_id
47
+ AND cd.status = 'active'
48
+ );
49
+
50
+ -- migrate:down
51
+
52
+ -- No-op: re-attaching the orphan feeds would require knowing which were
53
+ -- soft-deleted by this migration vs. by an operator action. The original
54
+ -- error condition is fixed in code; this migration is a one-shot data
55
+ -- cleanup. To recover specific feeds in prod, clear `deleted_at` on the
56
+ -- targeted rows manually and re-install the connector definition.
@@ -0,0 +1,27 @@
1
+ -- migrate:up
2
+
3
+ -- Bind PATs minted via /api/me/devices/mint-child-token to a specific
4
+ -- worker_id. Without this, a chrome-extension child PAT could post any
5
+ -- worker_id at /api/workers/poll and register a new device_workers row
6
+ -- under a different platform, bypassing the gateway's capability
7
+ -- authorization. The poll handler now refuses a request whose body's
8
+ -- `worker_id` doesn't match the PAT's bound `worker_id` (when non-NULL).
9
+ --
10
+ -- Legacy PATs (CLI tokens, OAuth-issued bearers via dynamic client
11
+ -- registration on the Mac/iOS bridges) keep worker_id = NULL; the poll
12
+ -- handler treats NULL as "no binding" and lets the body's value pass.
13
+
14
+ ALTER TABLE public.personal_access_tokens
15
+ ADD COLUMN IF NOT EXISTS worker_id text;
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_personal_access_tokens_worker_id
18
+ ON public.personal_access_tokens (worker_id)
19
+ WHERE worker_id IS NOT NULL;
20
+
21
+ COMMENT ON COLUMN public.personal_access_tokens.worker_id IS
22
+ 'Optional binding to a specific device_workers.worker_id. Set by /api/me/devices/mint-child-token. When non-NULL, /api/workers/poll requires the request body''s worker_id to match.';
23
+
24
+ -- migrate:down
25
+
26
+ DROP INDEX IF EXISTS public.idx_personal_access_tokens_worker_id;
27
+ ALTER TABLE public.personal_access_tokens DROP COLUMN IF EXISTS worker_id;
@@ -0,0 +1,30 @@
1
+ -- migrate:up
2
+
3
+ -- Archive watchers that are flagged `status = 'active'` but have no `agent_id`.
4
+ -- The scheduler (packages/server/src/watchers/automation.ts:469) already
5
+ -- filters them out with `WHERE w.agent_id IS NOT NULL`, so these rows are
6
+ -- zombies — visible in the API, redirect-bounced on the watcher detail
7
+ -- route, never actually executing.
8
+ --
9
+ -- 2026-05-17 prod audit: 28 active orphans across 11 orgs (most originated
10
+ -- from older create paths before `agent_id` was wired in). Archiving them
11
+ -- aligns the stored status with their actual execution state and lets the
12
+ -- `/$owner/watchers/$watcherId` redirect logic stop silently sending users
13
+ -- to /agents.
14
+ --
15
+ -- The write-time guard (this same PR, manage_watchers.ts) rejects new
16
+ -- watcher creates/updates that set a schedule without an agent, so the
17
+ -- orphan set can't grow again.
18
+
19
+ UPDATE public.watchers
20
+ SET status = 'archived',
21
+ updated_at = now()
22
+ WHERE status = 'active'
23
+ AND agent_id IS NULL;
24
+
25
+ -- migrate:down
26
+
27
+ -- No-op: this is a one-shot data cleanup. The original rows can be restored
28
+ -- manually if needed by assigning an agent_id and flipping status back to
29
+ -- 'active'; without an agent, status='active' is meaningless to the
30
+ -- scheduler.
@@ -0,0 +1,34 @@
1
+ -- migrate:up
2
+
3
+ -- Delete every watcher with `agent_id IS NULL` and enforce the column at
4
+ -- the schema level. These rows are leftovers from before the watcher
5
+ -- scheduler required `agent_id` (see watchers/automation.ts:469) — they
6
+ -- couldn't fire and the prior migration 20260517040000 had already
7
+ -- archived the active subset. Their dependent rows (windows, reactions,
8
+ -- versions, classifiers, field feedback) describe runtime state for
9
+ -- watchers that will never execute; ON DELETE CASCADE clears them.
10
+ -- `runs.watcher_id` is ON DELETE SET NULL, so the 21k historical run
11
+ -- records remain — they just lose the watcher linkage.
12
+ --
13
+ -- Application-level guards in manage_watchers.ts (handleCreate +
14
+ -- handleCreateFromVersion) reject new inserts without `agent_id`; the
15
+ -- NOT NULL constraint below is the matching DB-level enforcement so
16
+ -- bypass paths can't reintroduce the zombie state.
17
+
18
+ DELETE FROM public.watchers WHERE agent_id IS NULL;
19
+
20
+ ALTER TABLE public.watchers
21
+ ALTER COLUMN agent_id SET NOT NULL;
22
+
23
+ -- The existing index is partial (`WHERE agent_id IS NOT NULL`), which is
24
+ -- a tautology once the column is NOT NULL. Replace it with an unconditional
25
+ -- index so explain plans don't show the dead predicate.
26
+ DROP INDEX IF EXISTS public.idx_watchers_agent_id;
27
+ CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id);
28
+
29
+ -- migrate:down
30
+
31
+ ALTER TABLE public.watchers
32
+ ALTER COLUMN agent_id DROP NOT NULL;
33
+
34
+ -- No restoring deleted rows — this is forward-only data cleanup.
@@ -0,0 +1,78 @@
1
+ -- migrate:up
2
+
3
+ -- Adds the columns needed by the device-aware watcher dispatcher (see
4
+ -- lobu-ai/lobu#798 / #799):
5
+ --
6
+ -- * device_worker_id — pin a watcher to a specific device worker
7
+ -- (when its inputs live on that device).
8
+ -- * agent_kind — overrides the owning agent's default kind for
9
+ -- this watcher (e.g. "background", "notifier").
10
+ -- * notification_channel — where firings surface: canvas, OS notification,
11
+ -- or both.
12
+ -- * notification_priority — priority class used by the dispatcher's
13
+ -- interrupt budget.
14
+ -- * min_cooldown_seconds — minimum seconds between two firings of the
15
+ -- same watcher (0 = no cooldown).
16
+ -- * last_fired_at — last time this watcher actually dispatched
17
+ -- a notification/canvas item.
18
+ --
19
+ -- Also adds device_workers.notification_budget_per_day for the per-device
20
+ -- global interrupt budget. 10/day is a placeholder default; tune later.
21
+
22
+ ALTER TABLE public.watchers
23
+ ADD COLUMN device_worker_id uuid REFERENCES public.device_workers(id),
24
+ ADD COLUMN agent_kind text,
25
+ ADD COLUMN notification_channel text NOT NULL DEFAULT 'canvas',
26
+ ADD COLUMN notification_priority text NOT NULL DEFAULT 'normal',
27
+ ADD COLUMN min_cooldown_seconds integer NOT NULL DEFAULT 0,
28
+ ADD COLUMN last_fired_at timestamp with time zone;
29
+
30
+ ALTER TABLE public.watchers
31
+ ADD CONSTRAINT watchers_notification_channel_check
32
+ CHECK (notification_channel IN ('canvas', 'notification', 'both'));
33
+
34
+ ALTER TABLE public.watchers
35
+ ADD CONSTRAINT watchers_notification_priority_check
36
+ CHECK (notification_priority IN ('low', 'normal', 'high'));
37
+
38
+ ALTER TABLE public.watchers
39
+ ADD CONSTRAINT watchers_min_cooldown_seconds_nonneg
40
+ CHECK (min_cooldown_seconds >= 0);
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_watchers_device_worker_id
43
+ ON public.watchers (device_worker_id)
44
+ WHERE device_worker_id IS NOT NULL;
45
+
46
+ ALTER TABLE public.device_workers
47
+ ADD COLUMN notification_budget_per_day integer NOT NULL DEFAULT 10;
48
+
49
+ ALTER TABLE public.device_workers
50
+ ADD CONSTRAINT device_workers_notification_budget_per_day_nonneg
51
+ CHECK (notification_budget_per_day >= 0);
52
+
53
+ -- migrate:down
54
+
55
+ ALTER TABLE public.device_workers
56
+ DROP CONSTRAINT IF EXISTS device_workers_notification_budget_per_day_nonneg;
57
+
58
+ ALTER TABLE public.device_workers
59
+ DROP COLUMN IF EXISTS notification_budget_per_day;
60
+
61
+ DROP INDEX IF EXISTS public.idx_watchers_device_worker_id;
62
+
63
+ ALTER TABLE public.watchers
64
+ DROP CONSTRAINT IF EXISTS watchers_min_cooldown_seconds_nonneg;
65
+
66
+ ALTER TABLE public.watchers
67
+ DROP CONSTRAINT IF EXISTS watchers_notification_priority_check;
68
+
69
+ ALTER TABLE public.watchers
70
+ DROP CONSTRAINT IF EXISTS watchers_notification_channel_check;
71
+
72
+ ALTER TABLE public.watchers
73
+ DROP COLUMN IF EXISTS last_fired_at,
74
+ DROP COLUMN IF EXISTS min_cooldown_seconds,
75
+ DROP COLUMN IF EXISTS notification_priority,
76
+ DROP COLUMN IF EXISTS notification_channel,
77
+ DROP COLUMN IF EXISTS agent_kind,
78
+ DROP COLUMN IF EXISTS device_worker_id;
@@ -0,0 +1,55 @@
1
+ -- migrate:up
2
+
3
+ -- Goals primitive — a top-level handle that groups watchers under a single
4
+ -- user-facing intent (e.g. "Keep my CRM clean", "Watch competitors"). Each
5
+ -- watcher may optionally point at a goal; goals are the surface the
6
+ -- canvas/UI (#801) hangs off, while watchers stay the executable unit.
7
+ --
8
+ -- Schema notes:
9
+ -- - organization_id is `text` (the better-auth `organization.id`) to match
10
+ -- the rest of the schema; the issue body called it `integer`, but every
11
+ -- other org-scoped table (watchers, feeds, connections, …) uses text.
12
+ -- - `(organization_id, slug)` is unique so `lobu apply` can upsert by slug
13
+ -- the same way it does for watchers/agents.
14
+ -- - Goal-template loading from YAML is out of scope here; `template_key`
15
+ -- is just a free-form pointer for the future loader to claim.
16
+ -- - `metadata` is jsonb for forward-compat (icon, color, owner, etc.)
17
+ -- without another migration each time the UI grows a knob.
18
+
19
+ CREATE TABLE public.goals (
20
+ id bigserial PRIMARY KEY,
21
+ organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE,
22
+ slug text NOT NULL,
23
+ name text NOT NULL,
24
+ description text,
25
+ status text NOT NULL DEFAULT 'active',
26
+ template_key text,
27
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
28
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
29
+ updated_at timestamp with time zone NOT NULL DEFAULT now(),
30
+
31
+ CONSTRAINT goals_status_check
32
+ CHECK (status IN ('active', 'paused', 'archived')),
33
+ CONSTRAINT goals_org_slug_unique
34
+ UNIQUE (organization_id, slug)
35
+ );
36
+
37
+ CREATE INDEX idx_goals_organization_id
38
+ ON public.goals (organization_id);
39
+
40
+ -- Watcher → goal link. NULL means "ungrouped" (today's behavior). ON DELETE
41
+ -- SET NULL keeps the watcher alive when its goal is archived/deleted; the
42
+ -- watcher just becomes ungrouped.
43
+ ALTER TABLE public.watchers
44
+ ADD COLUMN goal_id bigint REFERENCES public.goals(id) ON DELETE SET NULL;
45
+
46
+ CREATE INDEX idx_watchers_goal_id
47
+ ON public.watchers (goal_id)
48
+ WHERE goal_id IS NOT NULL;
49
+
50
+ -- migrate:down
51
+
52
+ ALTER TABLE public.watchers
53
+ DROP COLUMN IF EXISTS goal_id;
54
+
55
+ DROP TABLE IF EXISTS public.goals;
@@ -0,0 +1,45 @@
1
+ -- migrate:up
2
+
3
+ -- Revert the goals primitive added in 20260517150000_goals_primitive.sql.
4
+ -- Agents already encapsulate the watcher-grouping use case via
5
+ -- watchers.agent_id; goals were a redundant nullable FK + parallel CRUD with
6
+ -- zero behavioral value. The primitive never shipped in a release.
7
+
8
+ DROP INDEX IF EXISTS idx_watchers_goal_id;
9
+
10
+ ALTER TABLE public.watchers
11
+ DROP COLUMN IF EXISTS goal_id;
12
+
13
+ DROP INDEX IF EXISTS idx_goals_organization_id;
14
+
15
+ DROP TABLE IF EXISTS public.goals;
16
+
17
+ -- migrate:down
18
+
19
+ CREATE TABLE public.goals (
20
+ id bigserial PRIMARY KEY,
21
+ organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE,
22
+ slug text NOT NULL,
23
+ name text NOT NULL,
24
+ description text,
25
+ status text NOT NULL DEFAULT 'active',
26
+ template_key text,
27
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
28
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
29
+ updated_at timestamp with time zone NOT NULL DEFAULT now(),
30
+
31
+ CONSTRAINT goals_status_check
32
+ CHECK (status IN ('active', 'paused', 'archived')),
33
+ CONSTRAINT goals_org_slug_unique
34
+ UNIQUE (organization_id, slug)
35
+ );
36
+
37
+ CREATE INDEX idx_goals_organization_id
38
+ ON public.goals (organization_id);
39
+
40
+ ALTER TABLE public.watchers
41
+ ADD COLUMN goal_id bigint REFERENCES public.goals(id) ON DELETE SET NULL;
42
+
43
+ CREATE INDEX idx_watchers_goal_id
44
+ ON public.watchers (goal_id)
45
+ WHERE goal_id IS NOT NULL;