@hotmeshio/long-tail 0.2.3 → 0.2.4

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.
@@ -43,27 +43,39 @@ const logger_1 = require("../logger");
43
43
  const SCHEMAS_DIR = path.join(__dirname, 'schemas');
44
44
  async function migrate() {
45
45
  const pool = (0, index_1.getPool)();
46
- // ensure migration tracking table
47
- await pool.query(`
48
- CREATE TABLE IF NOT EXISTS lt_migrations (
49
- id SERIAL PRIMARY KEY,
50
- name TEXT NOT NULL UNIQUE,
51
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
52
- );
53
- `);
54
- // find and sort migration files
55
- const files = fs.readdirSync(SCHEMAS_DIR)
56
- .filter(f => f.endsWith('.sql'))
57
- .sort();
58
- for (const file of files) {
59
- const { rows } = await pool.query('SELECT 1 FROM lt_migrations WHERE name = $1', [file]);
60
- if (rows.length === 0) {
61
- const sql = fs.readFileSync(path.join(SCHEMAS_DIR, file), 'utf-8');
62
- await pool.query(sql);
63
- await pool.query('INSERT INTO lt_migrations (name) VALUES ($1)', [file]);
64
- logger_1.loggerRegistry.info(`[migrate] applied: ${file}`);
46
+ // Advisory lock prevents concurrent containers from racing on migrations.
47
+ // Uses a dedicated client so the lock is held for the entire sequence.
48
+ const client = await pool.connect();
49
+ try {
50
+ await client.query('SELECT pg_advisory_lock(8675309)');
51
+ // ensure migration tracking table
52
+ await client.query(`
53
+ CREATE TABLE IF NOT EXISTS lt_migrations (
54
+ id SERIAL PRIMARY KEY,
55
+ name TEXT NOT NULL UNIQUE,
56
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
57
+ );
58
+ `);
59
+ // find and sort migration files
60
+ const files = fs.readdirSync(SCHEMAS_DIR)
61
+ .filter(f => f.endsWith('.sql'))
62
+ .sort();
63
+ for (const file of files) {
64
+ const { rows } = await client.query('SELECT 1 FROM lt_migrations WHERE name = $1', [file]);
65
+ if (rows.length === 0) {
66
+ const sql = fs.readFileSync(path.join(SCHEMAS_DIR, file), 'utf-8');
67
+ await client.query(sql);
68
+ await client.query('INSERT INTO lt_migrations (name) VALUES ($1)', [file]);
69
+ logger_1.loggerRegistry.info(`[migrate] applied: ${file}`);
70
+ }
65
71
  }
66
72
  }
73
+ finally {
74
+ // Advisory lock released when client is released (session-scoped),
75
+ // but release explicitly for clarity
76
+ await client.query('SELECT pg_advisory_unlock(8675309)').catch(() => { });
77
+ client.release();
78
+ }
67
79
  }
68
80
  // run directly: npx ts-node lib/db/migrate.ts
69
81
  if (require.main === module) {
@@ -132,13 +132,24 @@ async function ensureSystemBot() {
132
132
  const { rows } = await pool.query(sql_1.GET_USER_BY_EXTERNAL_ID, [SYSTEM_BOT_NAME]);
133
133
  if (rows.length > 0)
134
134
  return rows[0].id;
135
- const bot = await createBot({
136
- name: SYSTEM_BOT_NAME,
137
- display_name: 'System',
138
- description: 'System bot for cron and system-initiated workflows',
139
- roles: [{ role: 'system', type: 'superadmin' }],
140
- });
141
- return bot.id;
135
+ try {
136
+ const bot = await createBot({
137
+ name: SYSTEM_BOT_NAME,
138
+ display_name: 'System',
139
+ description: 'System bot for cron and system-initiated workflows',
140
+ roles: [{ role: 'system', type: 'superadmin' }],
141
+ });
142
+ return bot.id;
143
+ }
144
+ catch (err) {
145
+ // Concurrent container already created it — re-read
146
+ if (err.code === '23505') {
147
+ const { rows: retry } = await pool.query(sql_1.GET_USER_BY_EXTERNAL_ID, [SYSTEM_BOT_NAME]);
148
+ if (retry.length > 0)
149
+ return retry[0].id;
150
+ }
151
+ throw err;
152
+ }
142
153
  }
143
154
  // ── Helpers ────────────────────────────────────────────────────────────────
144
155
  function toBotRecord(user, description, createdBy) {
@@ -103,6 +103,10 @@ async function startWorkers(startConfig, workers, builtinMcpServerFactories) {
103
103
  logger_1.loggerRegistry.info('[long-tail] running migrations...');
104
104
  await (0, migrate_1.migrate)();
105
105
  const connection = buildConnection();
106
+ // Readonly mode: all user-provided workers are observers — skip crons, triggers, and agent seeding.
107
+ // System workers (mcpQuery, etc.) are always added by collectWorkers, so check the original config.
108
+ const userWorkers = startConfig.workers ?? [];
109
+ const isReadonly = userWorkers.length > 0 && userWorkers.every((w) => w.connection?.readonly);
106
110
  if (workers.length) {
107
111
  // Connect telemetry before HotMesh starts
108
112
  if (telemetry_1.telemetryRegistry.hasAdapter) {
@@ -165,13 +169,15 @@ async function startWorkers(startConfig, workers, builtinMcpServerFactories) {
165
169
  }
166
170
  ltConfig.invalidate();
167
171
  }
168
- // Start maintenance cron
169
- if (maintenance_1.maintenanceRegistry.hasConfig) {
172
+ // Start maintenance cron (skip in readonly/API mode)
173
+ if (maintenance_1.maintenanceRegistry.hasConfig && !isReadonly) {
170
174
  await maintenance_1.maintenanceRegistry.connect();
171
175
  logger_1.loggerRegistry.info('[long-tail] maintenance cron started');
172
176
  }
173
- // Start workflow cron schedules
174
- await cron_1.cronRegistry.connect();
177
+ // Start workflow cron schedules (skip in readonly/API mode)
178
+ if (!isReadonly) {
179
+ await cron_1.cronRegistry.connect();
180
+ }
175
181
  // Connect MCP adapter
176
182
  if (mcp_1.mcpRegistry.hasAdapter) {
177
183
  await mcp_1.mcpRegistry.connect();
@@ -281,19 +287,20 @@ async function startWorkers(startConfig, workers, builtinMcpServerFactories) {
281
287
  await events_1.eventRegistry.connect();
282
288
  logger_1.loggerRegistry.info('[long-tail] event adapters connected');
283
289
  }
284
- // Arm agent event subscriptions (after event adapters are connected)
285
- try {
286
- await agentTriggerRegistry.connect(callbackAdapter);
287
- }
288
- catch (err) {
289
- logger_1.loggerRegistry.warn(`[long-tail] agent trigger registry: ${err.message}`);
290
- }
291
- // Arm agent cron schedules
292
- try {
293
- await cron_1.cronRegistry.connectAgentCrons();
294
- }
295
- catch (err) {
296
- logger_1.loggerRegistry.warn(`[long-tail] agent cron schedules: ${err.message}`);
290
+ // Arm agent event subscriptions and crons (skip in readonly/API mode)
291
+ if (!isReadonly) {
292
+ try {
293
+ await agentTriggerRegistry.connect(callbackAdapter);
294
+ }
295
+ catch (err) {
296
+ logger_1.loggerRegistry.warn(`[long-tail] agent trigger registry: ${err.message}`);
297
+ }
298
+ try {
299
+ await cron_1.cronRegistry.connectAgentCrons();
300
+ }
301
+ catch (err) {
302
+ logger_1.loggerRegistry.warn(`[long-tail] agent cron schedules: ${err.message}`);
303
+ }
297
304
  }
298
305
  // Ensure system bot account exists for cron/system-initiated workflows
299
306
  const { ensureSystemBot } = await Promise.resolve().then(() => __importStar(require('../services/iam/bots')));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/long-tail",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Long Tail Workflows — Durable AI workflows with human-in-the-loop escalation. Powered by PostgreSQL.",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",