@hotmeshio/long-tail 0.1.11 → 0.1.13

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 (29) hide show
  1. package/README.md +1 -1
  2. package/build/api/escalations.js +7 -2
  3. package/build/examples/seed.js +50 -0
  4. package/build/lib/db/schemas/001_schema.sql +281 -106
  5. package/build/lib/db/schemas/002_seed.sql +56 -39
  6. package/build/services/mcp/client/connection.d.ts +13 -0
  7. package/build/services/mcp/client/connection.js +62 -0
  8. package/build/services/mcp/client/tools.js +20 -7
  9. package/build/services/mcp/server.js +31 -0
  10. package/build/services/yaml-workflow/workers/register.js +24 -4
  11. package/build/system/mcp-servers/human-queue.js +31 -0
  12. package/docs/cloud.md +123 -0
  13. package/package.json +3 -3
  14. package/build/lib/db/schemas/003_workflow_discovery.sql +0 -39
  15. package/build/lib/db/schemas/004_query_router.sql +0 -38
  16. package/build/lib/db/schemas/004_workflow_sets.sql +0 -29
  17. package/build/lib/db/schemas/005_triage_router.sql +0 -37
  18. package/build/lib/db/schemas/005_unique_graph_topic.sql +0 -7
  19. package/build/lib/db/schemas/006_oauth.sql +0 -50
  20. package/build/lib/db/schemas/007_security.sql +0 -27
  21. package/build/lib/db/schemas/008_bot_accounts.sql +0 -30
  22. package/build/lib/db/schemas/009_audit_trail.sql +0 -7
  23. package/build/lib/db/schemas/010_credential_providers.sql +0 -4
  24. package/build/lib/db/schemas/011_system_workflow_configs.sql +0 -37
  25. package/build/lib/db/schemas/012_drop_modality.sql +0 -6
  26. package/build/lib/db/schemas/013_execute_as.sql +0 -9
  27. package/build/lib/db/schemas/014_ephemeral_credentials.sql +0 -16
  28. package/build/lib/db/schemas/015_knowledge.sql +0 -23
  29. package/build/lib/db/schemas/016_streamable_http.sql +0 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Long Tail
2
2
 
3
- Turn your PostgreSQL database into a workflow engine with identity-aware durable execution, human-in-the-loop escalation, and MCP tool orchestration.
3
+ Long Tail turns your PostgreSQL database into a workflow engine where human and AI work share the same durable execution path. Hand off, pull back, escalate, automate without rewriting systems or losing continuity.
4
4
 
5
5
  ```bash
6
6
  npm install @hotmeshio/long-tail
@@ -730,7 +730,12 @@ async function resolveEscalation(input, auth) {
730
730
  const handle = await client.workflow.getHandle(signalRouting.taskQueue, signalRouting.workflowType, signalRouting.workflowId);
731
731
  await handle.signal(signalRouting.signalId, signalPayload);
732
732
  }
733
- await escalationService.resolveEscalation(escalation.id, resolverPayload);
733
+ // For YAML workflows, the resolve worker inside the workflow calls
734
+ // claim_and_resolve to close the escalation transactionally. Only resolve
735
+ // here for Durable workflows that lack an in-workflow resolve step.
736
+ if (signalRouting.engine !== 'yaml') {
737
+ await escalationService.resolveEscalation(escalation.id, resolverPayload);
738
+ }
734
739
  (0, publish_1.publishEscalationEvent)({
735
740
  type: 'escalation.resolved',
736
741
  source: 'api',
@@ -740,7 +745,7 @@ async function resolveEscalation(input, auth) {
740
745
  taskId: escalation.task_id,
741
746
  escalationId: escalation.id,
742
747
  originId: escalation.origin_id ?? undefined,
743
- status: 'resolved',
748
+ status: signalRouting.engine === 'yaml' ? 'signaled' : 'resolved',
744
749
  });
745
750
  return {
746
751
  status: 200,
@@ -4,6 +4,7 @@ exports.seedExamples = seedExamples;
4
4
  const hotmesh_1 = require("@hotmeshio/hotmesh");
5
5
  const defaults_1 = require("../modules/defaults");
6
6
  const logger_1 = require("../lib/logger");
7
+ const db_1 = require("../lib/db");
7
8
  const user_1 = require("../services/user");
8
9
  const roles_1 = require("../services/user/roles");
9
10
  const role_1 = require("../services/role");
@@ -211,11 +212,60 @@ async function seedEscalationChains() {
211
212
  }
212
213
  logger_1.loggerRegistry.info(`[examples] escalation chains verified (${SEED_CHAINS.length} entries)`);
213
214
  }
215
+ /**
216
+ * Seed example workflow configs into lt_config_workflows.
217
+ * Previously done by 002_seed.sql (which ran unconditionally).
218
+ * Now only runs when examples: true.
219
+ */
220
+ async function seedExampleConfigs() {
221
+ const pool = (0, db_1.getPool)();
222
+ await pool.query(`
223
+ INSERT INTO lt_config_workflows
224
+ (workflow_type, task_queue, default_role, invocable, description, tool_tags, envelope_schema, resolver_schema)
225
+ VALUES
226
+ ('reviewContent', 'long-tail-examples', 'reviewer', true,
227
+ 'Content review — AI-powered moderation with human escalation for low-confidence results',
228
+ ARRAY['document-processing', 'vision', 'ocr', 'translation'],
229
+ '{"data": {"contentId": "article-001", "content": "Content to review...", "contentType": "article"}, "metadata": {"source": "dashboard"}}'::jsonb,
230
+ '{"approved": true, "analysis": {"confidence": 0.95, "flags": [], "summary": "Manually reviewed and approved."}}'::jsonb),
231
+ ('verifyDocument', 'long-tail-examples', 'reviewer', true,
232
+ 'Document verification — AI Vision analyzes identity documents',
233
+ ARRAY['document-processing', 'vision', 'ocr', 'translation'],
234
+ '{"data": {"documentId": "doc-001", "documentUrl": "https://example.com/doc.jpg", "documentType": "drivers_license", "memberId": "member-12345"}, "metadata": {"source": "dashboard"}}'::jsonb,
235
+ '{"memberId": "", "extractedInfo": {}, "validationResult": "match", "confidence": 1.0}'::jsonb),
236
+ ('processClaim', 'long-tail-examples', 'reviewer', true,
237
+ 'Insurance claim processing — document analysis, validation, and human review',
238
+ ARRAY['document-processing', 'vision', 'database', 'query'],
239
+ '{"data": {"claimId": "CLM-2024-001", "claimantId": "POL-5551234", "claimType": "auto_collision", "amount": 12500, "documents": ["incident_report.pdf", "photo_evidence.jpg"]}, "metadata": {"source": "dashboard"}}'::jsonb,
240
+ '{"approved": true, "analysis": {"confidence": 0.92, "flags": [], "summary": "Documents reviewed and verified."}, "status": "resolved"}'::jsonb),
241
+ ('kitchenSink', 'long-tail-examples', 'reviewer', true,
242
+ 'Kitchen sink — demonstrates sleep, signals, parallel activities, escalation, and every durable primitive',
243
+ '{}',
244
+ '{"data": {"name": "World", "mode": "full"}, "metadata": {"source": "dashboard"}}'::jsonb,
245
+ NULL),
246
+ ('basicSignal', 'long-tail-examples', 'reviewer', true,
247
+ 'Signal-based escalation — workflow stays running while waiting for human input via conditionLT',
248
+ '{}',
249
+ '{"data": {"message": "Deployment approval needed for v2.1.0", "role": "reviewer"}, "metadata": {"certified": false, "source": "dashboard"}}'::jsonb,
250
+ '{"properties": {"approved": {"type": "boolean", "default": false, "description": "Approve this deployment?"}, "notes": {"type": "string", "default": "", "description": "Reviewer notes — visible to the workflow author"}}}'::jsonb)
251
+ ON CONFLICT (workflow_type) DO NOTHING
252
+ `);
253
+ // Assign roles to example workflows
254
+ await pool.query(`
255
+ INSERT INTO lt_config_roles (workflow_type, role)
256
+ SELECT workflow_type, unnest(ARRAY['reviewer', 'engineer', 'admin'])
257
+ FROM lt_config_workflows
258
+ WHERE workflow_type IN ('reviewContent', 'verifyDocument', 'processClaim', 'kitchenSink')
259
+ ON CONFLICT (workflow_type, role) DO NOTHING
260
+ `);
261
+ logger_1.loggerRegistry.info('[examples] workflow configs seeded');
262
+ }
214
263
  /**
215
264
  * Seed example workflows so the dashboard tells a story immediately.
216
265
  * Called automatically when `examples: true` is set in the start config.
217
266
  */
218
267
  async function seedExamples(client) {
268
+ await seedExampleConfigs();
219
269
  await seedRoles();
220
270
  await seedUsers();
221
271
  await seedEscalationChains();
@@ -1,5 +1,5 @@
1
1
  -- Long Tail Workflows: Schema
2
- -- Tasks track workflow executions; escalations track human interventions.
2
+ -- All tables, indexes, triggers, and functions.
3
3
 
4
4
  -- ─── updated_at trigger ─────────────────────────────────────────────────────
5
5
 
@@ -28,28 +28,31 @@ ON CONFLICT DO NOTHING;
28
28
  -- ─── lt_tasks ────────────────────────────────────────────────────────────────
29
29
 
30
30
  CREATE TABLE IF NOT EXISTS lt_tasks (
31
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32
- workflow_id TEXT NOT NULL,
33
- workflow_type TEXT NOT NULL,
34
- lt_type TEXT NOT NULL,
35
- task_queue TEXT,
36
- status TEXT NOT NULL DEFAULT 'pending',
37
- priority INTEGER NOT NULL DEFAULT 2,
38
- signal_id TEXT NOT NULL,
31
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32
+ workflow_id TEXT NOT NULL,
33
+ workflow_type TEXT NOT NULL,
34
+ lt_type TEXT NOT NULL,
35
+ task_queue TEXT,
36
+ status TEXT NOT NULL DEFAULT 'pending',
37
+ priority INTEGER NOT NULL DEFAULT 2,
38
+ signal_id TEXT NOT NULL,
39
39
  parent_workflow_id TEXT NOT NULL,
40
- origin_id TEXT,
41
- parent_id TEXT,
42
- started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
43
- completed_at TIMESTAMPTZ,
44
- envelope TEXT NOT NULL,
45
- metadata JSONB,
46
- error TEXT,
47
- milestones JSONB NOT NULL DEFAULT '[]'::JSONB,
48
- data TEXT,
49
- trace_id TEXT,
50
- span_id TEXT,
51
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
52
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
40
+ origin_id TEXT,
41
+ parent_id TEXT,
42
+ started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
43
+ completed_at TIMESTAMPTZ,
44
+ envelope TEXT NOT NULL,
45
+ metadata JSONB,
46
+ error TEXT,
47
+ milestones JSONB NOT NULL DEFAULT '[]'::JSONB,
48
+ data TEXT,
49
+ trace_id TEXT,
50
+ span_id TEXT,
51
+ initiated_by UUID,
52
+ principal_type TEXT DEFAULT 'user',
53
+ executing_as TEXT,
54
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
55
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
53
56
  );
54
57
 
55
58
  CREATE INDEX IF NOT EXISTS idx_lt_tasks_status_type ON lt_tasks (status, workflow_type, created_at DESC);
@@ -61,39 +64,80 @@ CREATE INDEX IF NOT EXISTS idx_lt_tasks_origin ON lt_tasks (origin_id, created_a
61
64
  CREATE INDEX IF NOT EXISTS idx_lt_tasks_workflow_id ON lt_tasks (workflow_id);
62
65
  CREATE INDEX IF NOT EXISTS idx_lt_tasks_origin_id ON lt_tasks (origin_id) WHERE origin_id IS NOT NULL;
63
66
  CREATE INDEX IF NOT EXISTS idx_lt_tasks_trace ON lt_tasks (trace_id) WHERE trace_id IS NOT NULL;
67
+ CREATE INDEX IF NOT EXISTS idx_lt_tasks_initiated_by ON lt_tasks (initiated_by) WHERE initiated_by IS NOT NULL;
64
68
 
65
69
  CREATE OR REPLACE TRIGGER trg_lt_tasks_updated_at
66
70
  BEFORE UPDATE ON lt_tasks
67
71
  FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
68
72
 
73
+ -- ─── lt_users ────────────────────────────────────────────────────────────────
74
+
75
+ CREATE TABLE IF NOT EXISTS lt_users (
76
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
77
+ external_id TEXT UNIQUE NOT NULL,
78
+ email TEXT,
79
+ display_name TEXT,
80
+ password_hash TEXT,
81
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
82
+ account_type TEXT NOT NULL DEFAULT 'user' CHECK (account_type IN ('user', 'bot')),
83
+ oauth_provider TEXT,
84
+ oauth_provider_id TEXT,
85
+ metadata JSONB,
86
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
87
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
88
+ );
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_lt_users_status ON lt_users (status);
91
+ CREATE INDEX IF NOT EXISTS idx_lt_users_oauth
92
+ ON lt_users (oauth_provider, oauth_provider_id)
93
+ WHERE oauth_provider IS NOT NULL;
94
+
95
+ CREATE TRIGGER lt_users_updated_at
96
+ BEFORE UPDATE ON lt_users
97
+ FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
98
+
99
+ -- ─── lt_user_roles ───────────────────────────────────────────────────────────
100
+
101
+ CREATE TABLE IF NOT EXISTS lt_user_roles (
102
+ user_id UUID NOT NULL REFERENCES lt_users(id) ON DELETE CASCADE,
103
+ role TEXT NOT NULL REFERENCES lt_roles(role),
104
+ type TEXT NOT NULL DEFAULT 'member' CHECK (type IN ('superadmin', 'admin', 'member')),
105
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
106
+ PRIMARY KEY (user_id, role)
107
+ );
108
+
109
+ CREATE INDEX IF NOT EXISTS idx_lt_user_roles_type ON lt_user_roles (type);
110
+ CREATE INDEX IF NOT EXISTS idx_lt_user_roles_user_id ON lt_user_roles (user_id);
111
+
69
112
  -- ─── lt_escalations ─────────────────────────────────────────────────────────
70
113
 
71
114
  CREATE TABLE IF NOT EXISTS lt_escalations (
72
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
73
- type TEXT NOT NULL,
74
- subtype TEXT NOT NULL,
75
- description TEXT,
76
- status TEXT NOT NULL DEFAULT 'pending',
77
- priority INTEGER NOT NULL DEFAULT 2,
78
- task_id UUID REFERENCES lt_tasks(id),
79
- origin_id TEXT,
80
- parent_id TEXT,
81
- workflow_id TEXT,
82
- task_queue TEXT,
83
- workflow_type TEXT,
84
- role TEXT NOT NULL REFERENCES lt_roles(role),
85
- assigned_to TEXT,
86
- assigned_until TIMESTAMPTZ,
87
- resolved_at TIMESTAMPTZ,
88
- claimed_at TIMESTAMPTZ,
89
- envelope TEXT NOT NULL,
90
- metadata JSONB,
115
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
116
+ type TEXT NOT NULL,
117
+ subtype TEXT NOT NULL,
118
+ description TEXT,
119
+ status TEXT NOT NULL DEFAULT 'pending',
120
+ priority INTEGER NOT NULL DEFAULT 2,
121
+ task_id UUID REFERENCES lt_tasks(id),
122
+ origin_id TEXT,
123
+ parent_id TEXT,
124
+ workflow_id TEXT,
125
+ task_queue TEXT,
126
+ workflow_type TEXT,
127
+ role TEXT NOT NULL REFERENCES lt_roles(role),
128
+ assigned_to TEXT,
129
+ assigned_until TIMESTAMPTZ,
130
+ resolved_at TIMESTAMPTZ,
131
+ claimed_at TIMESTAMPTZ,
132
+ created_by UUID,
133
+ envelope TEXT NOT NULL,
134
+ metadata JSONB,
91
135
  escalation_payload TEXT,
92
- resolver_payload TEXT,
93
- trace_id TEXT,
94
- span_id TEXT,
95
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
96
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
136
+ resolver_payload TEXT,
137
+ trace_id TEXT,
138
+ span_id TEXT,
139
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
140
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
97
141
  );
98
142
 
99
143
  CREATE INDEX IF NOT EXISTS idx_lt_escalations_available ON lt_escalations (status, role, assigned_until, created_at DESC);
@@ -113,44 +157,12 @@ CREATE INDEX IF NOT EXISTS idx_lt_escalations_trace ON lt_escalations (trace_id)
113
157
  CREATE INDEX IF NOT EXISTS idx_lt_escalations_created_desc ON lt_escalations (created_at DESC);
114
158
  CREATE INDEX IF NOT EXISTS idx_lt_escalations_updated_desc ON lt_escalations (updated_at DESC);
115
159
  CREATE INDEX IF NOT EXISTS idx_lt_escalations_priority_desc ON lt_escalations (priority DESC, created_at DESC);
160
+ CREATE INDEX IF NOT EXISTS idx_lt_escalations_created_by ON lt_escalations (created_by) WHERE created_by IS NOT NULL;
116
161
 
117
162
  CREATE OR REPLACE TRIGGER trg_lt_escalations_updated_at
118
163
  BEFORE UPDATE ON lt_escalations
119
164
  FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
120
165
 
121
- -- ─── lt_users ────────────────────────────────────────────────────────────────
122
-
123
- CREATE TABLE IF NOT EXISTS lt_users (
124
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
125
- external_id TEXT UNIQUE NOT NULL,
126
- email TEXT,
127
- display_name TEXT,
128
- password_hash TEXT,
129
- status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
130
- metadata JSONB,
131
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
132
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
133
- );
134
-
135
- CREATE TRIGGER lt_users_updated_at
136
- BEFORE UPDATE ON lt_users
137
- FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
138
-
139
- CREATE INDEX IF NOT EXISTS idx_lt_users_status ON lt_users (status);
140
-
141
- -- ─── lt_user_roles ───────────────────────────────────────────────────────────
142
-
143
- CREATE TABLE IF NOT EXISTS lt_user_roles (
144
- user_id UUID NOT NULL REFERENCES lt_users(id) ON DELETE CASCADE,
145
- role TEXT NOT NULL REFERENCES lt_roles(role),
146
- type TEXT NOT NULL DEFAULT 'member' CHECK (type IN ('superadmin', 'admin', 'member')),
147
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
148
- PRIMARY KEY (user_id, role)
149
- );
150
-
151
- CREATE INDEX IF NOT EXISTS idx_lt_user_roles_type ON lt_user_roles (type);
152
- CREATE INDEX IF NOT EXISTS idx_lt_user_roles_user_id ON lt_user_roles (user_id);
153
-
154
166
  -- ─── lt_config_workflows ────────────────────────────────────────────────────
155
167
 
156
168
  CREATE TABLE IF NOT EXISTS lt_config_workflows (
@@ -193,24 +205,38 @@ CREATE TABLE IF NOT EXISTS lt_config_invocation_roles (
193
205
  UNIQUE(workflow_type, role)
194
206
  );
195
207
 
208
+ -- ─── lt_config_role_escalations ─────────────────────────────────────────────
209
+
210
+ CREATE TABLE IF NOT EXISTS lt_config_role_escalations (
211
+ source_role TEXT NOT NULL REFERENCES lt_roles(role),
212
+ target_role TEXT NOT NULL REFERENCES lt_roles(role),
213
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
214
+ PRIMARY KEY (source_role, target_role)
215
+ );
216
+
217
+ CREATE INDEX IF NOT EXISTS idx_lt_config_role_escalations_source
218
+ ON lt_config_role_escalations (source_role);
219
+
196
220
  -- ─── lt_mcp_servers ─────────────────────────────────────────────────────────
197
221
 
198
222
  CREATE TABLE IF NOT EXISTS lt_mcp_servers (
199
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
200
- name TEXT UNIQUE NOT NULL,
201
- description TEXT,
202
- transport_type TEXT NOT NULL CHECK (transport_type IN ('stdio', 'sse')),
203
- transport_config JSONB NOT NULL DEFAULT '{}'::JSONB,
204
- auto_connect BOOLEAN NOT NULL DEFAULT false,
205
- tool_manifest JSONB,
206
- status TEXT NOT NULL DEFAULT 'registered'
207
- CHECK (status IN ('registered', 'connected', 'error', 'disconnected')),
208
- last_connected_at TIMESTAMPTZ,
209
- metadata JSONB,
210
- tags TEXT[] NOT NULL DEFAULT '{}',
211
- compile_hints TEXT,
212
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
213
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
223
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
224
+ name TEXT UNIQUE NOT NULL,
225
+ description TEXT,
226
+ transport_type TEXT NOT NULL CHECK (transport_type IN ('stdio', 'sse', 'streamable-http')),
227
+ transport_config JSONB NOT NULL DEFAULT '{}'::JSONB,
228
+ auto_connect BOOLEAN NOT NULL DEFAULT false,
229
+ tool_manifest JSONB,
230
+ status TEXT NOT NULL DEFAULT 'registered'
231
+ CHECK (status IN ('registered', 'connected', 'error', 'disconnected')),
232
+ last_connected_at TIMESTAMPTZ,
233
+ metadata JSONB,
234
+ tags TEXT[] NOT NULL DEFAULT '{}',
235
+ compile_hints TEXT,
236
+ required_scopes TEXT[] NOT NULL DEFAULT '{}',
237
+ credential_providers TEXT[] NOT NULL DEFAULT '{}',
238
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
239
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
214
240
  );
215
241
 
216
242
  CREATE INDEX IF NOT EXISTS idx_lt_mcp_servers_name ON lt_mcp_servers (name);
@@ -222,18 +248,6 @@ CREATE OR REPLACE TRIGGER trg_lt_mcp_servers_updated_at
222
248
  BEFORE UPDATE ON lt_mcp_servers
223
249
  FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
224
250
 
225
- -- ─── lt_config_role_escalations ─────────────────────────────────────────────
226
-
227
- CREATE TABLE IF NOT EXISTS lt_config_role_escalations (
228
- source_role TEXT NOT NULL REFERENCES lt_roles(role),
229
- target_role TEXT NOT NULL REFERENCES lt_roles(role),
230
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
231
- PRIMARY KEY (source_role, target_role)
232
- );
233
-
234
- CREATE INDEX IF NOT EXISTS idx_lt_config_role_escalations_source
235
- ON lt_config_role_escalations (source_role);
236
-
237
251
  -- ─── lt_yaml_workflows ──────────────────────────────────────────────────────
238
252
 
239
253
  CREATE TABLE IF NOT EXISTS lt_yaml_workflows (
@@ -261,6 +275,12 @@ CREATE TABLE IF NOT EXISTS lt_yaml_workflows (
261
275
  cron_schedule TEXT,
262
276
  cron_envelope JSONB,
263
277
  execute_as TEXT,
278
+ original_prompt TEXT,
279
+ category TEXT,
280
+ search_vector TSVECTOR,
281
+ set_id UUID,
282
+ set_role TEXT CHECK (set_role IN ('leaf', 'composition', 'router')),
283
+ set_build_order INTEGER,
264
284
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
265
285
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
266
286
  );
@@ -268,11 +288,36 @@ CREATE TABLE IF NOT EXISTS lt_yaml_workflows (
268
288
  CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_status ON lt_yaml_workflows (status);
269
289
  CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_app_id ON lt_yaml_workflows (app_id);
270
290
  CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_tags ON lt_yaml_workflows USING GIN (tags);
291
+ CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_search ON lt_yaml_workflows USING GIN (search_vector);
292
+ CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_category ON lt_yaml_workflows (category) WHERE category IS NOT NULL;
293
+ CREATE INDEX IF NOT EXISTS idx_lt_yaml_workflows_set_id ON lt_yaml_workflows (set_id) WHERE set_id IS NOT NULL;
294
+
295
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_lt_yaml_workflows_app_topic_unique
296
+ ON lt_yaml_workflows (app_id, graph_topic)
297
+ WHERE status != 'archived';
271
298
 
272
299
  CREATE OR REPLACE TRIGGER trg_lt_yaml_workflows_updated_at
273
300
  BEFORE UPDATE ON lt_yaml_workflows
274
301
  FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
275
302
 
303
+ -- Full-text search trigger
304
+ CREATE OR REPLACE FUNCTION lt_yaml_workflows_search_vector_update()
305
+ RETURNS TRIGGER AS $$
306
+ BEGIN
307
+ NEW.search_vector :=
308
+ setweight(to_tsvector('english', coalesce(NEW.name, '')), 'A') ||
309
+ setweight(to_tsvector('english', coalesce(NEW.original_prompt, '')), 'A') ||
310
+ setweight(to_tsvector('english', coalesce(NEW.description, '')), 'B') ||
311
+ setweight(to_tsvector('english', coalesce(NEW.category, '')), 'C') ||
312
+ setweight(to_tsvector('english', coalesce(array_to_string(NEW.tags, ' '), '')), 'C');
313
+ RETURN NEW;
314
+ END;
315
+ $$ LANGUAGE plpgsql;
316
+
317
+ CREATE OR REPLACE TRIGGER trg_lt_yaml_workflows_search_vector
318
+ BEFORE INSERT OR UPDATE ON lt_yaml_workflows
319
+ FOR EACH ROW EXECUTE FUNCTION lt_yaml_workflows_search_vector_update();
320
+
276
321
  -- ─── lt_yaml_workflow_versions ──────────────────────────────────────────────
277
322
 
278
323
  CREATE TABLE IF NOT EXISTS lt_yaml_workflow_versions (
@@ -292,6 +337,136 @@ CREATE TABLE IF NOT EXISTS lt_yaml_workflow_versions (
292
337
  CREATE INDEX IF NOT EXISTS idx_lt_yaml_wf_versions_workflow
293
338
  ON lt_yaml_workflow_versions (workflow_id, version DESC);
294
339
 
340
+ -- ─── lt_workflow_sets ───────────────────────────────────────────────────────
341
+
342
+ CREATE TABLE IF NOT EXISTS lt_workflow_sets (
343
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
344
+ name TEXT UNIQUE NOT NULL,
345
+ description TEXT,
346
+ specification TEXT NOT NULL,
347
+ plan JSONB NOT NULL DEFAULT '[]'::JSONB,
348
+ namespaces TEXT[] NOT NULL DEFAULT '{}',
349
+ status TEXT NOT NULL DEFAULT 'planning'
350
+ CHECK (status IN ('planning','planned','building','deploying','completed','failed')),
351
+ source_workflow_id TEXT,
352
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
353
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
354
+ );
355
+
356
+ CREATE OR REPLACE TRIGGER trg_lt_workflow_sets_updated_at
357
+ BEFORE UPDATE ON lt_workflow_sets
358
+ FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
359
+
360
+ -- Add FK for lt_yaml_workflows.set_id (table already exists, add constraint)
361
+ DO $$ BEGIN
362
+ ALTER TABLE lt_yaml_workflows ADD CONSTRAINT fk_lt_yaml_workflows_set_id
363
+ FOREIGN KEY (set_id) REFERENCES lt_workflow_sets(id) ON DELETE SET NULL;
364
+ EXCEPTION
365
+ WHEN duplicate_object THEN NULL;
366
+ END $$;
367
+
368
+ -- ─── lt_oauth_tokens ────────────────────────────────────────────────────────
369
+
370
+ CREATE TABLE IF NOT EXISTS lt_oauth_tokens (
371
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
372
+ user_id UUID NOT NULL REFERENCES lt_users(id) ON DELETE CASCADE,
373
+ provider TEXT NOT NULL,
374
+ label TEXT NOT NULL DEFAULT 'default',
375
+ access_token_enc TEXT NOT NULL,
376
+ refresh_token_enc TEXT,
377
+ token_type TEXT NOT NULL DEFAULT 'bearer',
378
+ scopes TEXT[] NOT NULL DEFAULT '{}',
379
+ expires_at TIMESTAMPTZ,
380
+ provider_user_id TEXT NOT NULL,
381
+ provider_email TEXT,
382
+ metadata JSONB,
383
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
384
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
385
+ UNIQUE (user_id, provider, label)
386
+ );
387
+
388
+ CREATE INDEX IF NOT EXISTS idx_lt_oauth_tokens_provider
389
+ ON lt_oauth_tokens (provider, user_id);
390
+
391
+ CREATE OR REPLACE TRIGGER trg_lt_oauth_tokens_updated_at
392
+ BEFORE UPDATE ON lt_oauth_tokens
393
+ FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
394
+
395
+ -- ─── lt_service_tokens ──────────────────────────────────────────────────────
396
+
397
+ CREATE TABLE IF NOT EXISTS lt_service_tokens (
398
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
399
+ name TEXT UNIQUE NOT NULL,
400
+ token_hash TEXT NOT NULL,
401
+ server_id UUID REFERENCES lt_mcp_servers(id) ON DELETE CASCADE,
402
+ scopes TEXT[] NOT NULL DEFAULT '{}',
403
+ expires_at TIMESTAMPTZ,
404
+ last_used_at TIMESTAMPTZ,
405
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
406
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
407
+ );
408
+
409
+ CREATE INDEX IF NOT EXISTS idx_lt_service_tokens_server
410
+ ON lt_service_tokens (server_id);
411
+
412
+ CREATE OR REPLACE TRIGGER trg_lt_service_tokens_updated_at
413
+ BEFORE UPDATE ON lt_service_tokens
414
+ FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
415
+
416
+ -- ─── lt_bot_api_keys ────────────────────────────────────────────────────────
417
+
418
+ CREATE TABLE IF NOT EXISTS lt_bot_api_keys (
419
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
420
+ name TEXT NOT NULL,
421
+ user_id UUID NOT NULL REFERENCES lt_users(id) ON DELETE CASCADE,
422
+ key_hash TEXT NOT NULL,
423
+ scopes TEXT[] NOT NULL DEFAULT '{}',
424
+ expires_at TIMESTAMPTZ,
425
+ last_used_at TIMESTAMPTZ,
426
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
427
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
428
+ UNIQUE (user_id, name)
429
+ );
430
+
431
+ CREATE INDEX IF NOT EXISTS idx_bot_api_keys_user_id ON lt_bot_api_keys (user_id);
432
+
433
+ -- ─── lt_ephemeral_credentials ───────────────────────────────────────────────
434
+
435
+ CREATE TABLE IF NOT EXISTS lt_ephemeral_credentials (
436
+ token UUID PRIMARY KEY DEFAULT gen_random_uuid(),
437
+ value BYTEA NOT NULL,
438
+ label TEXT,
439
+ max_uses INTEGER NOT NULL DEFAULT 0,
440
+ use_count INTEGER NOT NULL DEFAULT 0,
441
+ expires_at TIMESTAMPTZ,
442
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
443
+ );
444
+
445
+ CREATE INDEX IF NOT EXISTS idx_lt_ephemeral_expiry
446
+ ON lt_ephemeral_credentials (expires_at)
447
+ WHERE expires_at IS NOT NULL;
448
+
449
+ -- ─── lt_knowledge ───────────────────────────────────────────────────────────
450
+
451
+ CREATE TABLE IF NOT EXISTS lt_knowledge (
452
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
453
+ domain TEXT NOT NULL,
454
+ key TEXT NOT NULL,
455
+ data JSONB NOT NULL DEFAULT '{}',
456
+ tags TEXT[] NOT NULL DEFAULT '{}',
457
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
458
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
459
+ UNIQUE(domain, key)
460
+ );
461
+
462
+ CREATE INDEX IF NOT EXISTS idx_lt_knowledge_domain ON lt_knowledge (domain);
463
+ CREATE INDEX IF NOT EXISTS idx_lt_knowledge_tags ON lt_knowledge USING GIN (tags);
464
+ CREATE INDEX IF NOT EXISTS idx_lt_knowledge_data ON lt_knowledge USING GIN (data);
465
+
466
+ CREATE TRIGGER lt_knowledge_updated_at
467
+ BEFORE UPDATE ON lt_knowledge
468
+ FOR EACH ROW EXECUTE FUNCTION lt_set_updated_at();
469
+
295
470
  -- ─── lt_namespaces ──────────────────────────────────────────────────────────
296
471
 
297
472
  CREATE TABLE IF NOT EXISTS lt_namespaces (