@hotmeshio/long-tail 0.2.3 → 0.2.5
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.
- package/build/lib/db/migrate.js +31 -19
- package/build/services/cron/index.d.ts +7 -1
- package/build/services/cron/index.js +8 -13
- package/build/services/iam/bots.js +18 -7
- package/build/services/maintenance/index.d.ts +3 -1
- package/build/services/maintenance/index.js +3 -14
- package/build/start/workers.js +24 -17
- package/package.json +1 -1
package/build/lib/db/migrate.js
CHANGED
|
@@ -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
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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) {
|
|
@@ -69,7 +69,13 @@ declare class LTCronRegistry {
|
|
|
69
69
|
*/
|
|
70
70
|
connectAgentCrons(): Promise<void>;
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
72
|
+
* Disconnect on graceful shutdown. Clears the local registry so this
|
|
73
|
+
* container stops consuming cron streams. Does NOT call Virtual.interrupt()
|
|
74
|
+
* — cron jobs are durable rows shared across the fleet. Another container
|
|
75
|
+
* (or this one on restart) will continue servicing them.
|
|
76
|
+
*
|
|
77
|
+
* Use stopCron/stopAgentCrons/stopYamlCron for intentional permanent kills
|
|
78
|
+
* (e.g. user deletes an agent or changes a schedule via the dashboard).
|
|
73
79
|
*/
|
|
74
80
|
disconnect(): Promise<void>;
|
|
75
81
|
/**
|
|
@@ -444,21 +444,16 @@ class LTCronRegistry {
|
|
|
444
444
|
logger_1.loggerRegistry.info(`[lt-cron] ${armed} agent schedule(s) armed`);
|
|
445
445
|
}
|
|
446
446
|
/**
|
|
447
|
-
*
|
|
447
|
+
* Disconnect on graceful shutdown. Clears the local registry so this
|
|
448
|
+
* container stops consuming cron streams. Does NOT call Virtual.interrupt()
|
|
449
|
+
* — cron jobs are durable rows shared across the fleet. Another container
|
|
450
|
+
* (or this one on restart) will continue servicing them.
|
|
451
|
+
*
|
|
452
|
+
* Use stopCron/stopAgentCrons/stopYamlCron for intentional permanent kills
|
|
453
|
+
* (e.g. user deletes an agent or changes a schedule via the dashboard).
|
|
448
454
|
*/
|
|
449
455
|
async disconnect() {
|
|
450
|
-
|
|
451
|
-
if (key.startsWith('agent:')) {
|
|
452
|
-
const agentId = key.split(':')[1].split('-')[0];
|
|
453
|
-
await this.stopAgentCrons(agentId);
|
|
454
|
-
}
|
|
455
|
-
else if (key.startsWith('yaml:')) {
|
|
456
|
-
await this.stopYamlCron(key.replace('yaml:', ''));
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
await this.stopCron(key);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
456
|
+
this.activeCrons.clear();
|
|
462
457
|
this.connected = false;
|
|
463
458
|
}
|
|
464
459
|
/**
|
|
@@ -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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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) {
|
|
@@ -22,7 +22,9 @@ declare class LTMaintenanceRegistry {
|
|
|
22
22
|
*/
|
|
23
23
|
connect(): Promise<void>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Disconnect on graceful shutdown. Stops consuming but leaves the cron
|
|
26
|
+
* row alive — it's a durable job shared across the fleet. Another
|
|
27
|
+
* container (or this one on restart) will continue servicing it.
|
|
26
28
|
*/
|
|
27
29
|
disconnect(): Promise<void>;
|
|
28
30
|
/**
|
|
@@ -140,22 +140,11 @@ class LTMaintenanceRegistry {
|
|
|
140
140
|
logger_1.loggerRegistry.info(`[lt-maintenance] cron started (schedule: ${schedule})`);
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* Disconnect on graceful shutdown. Stops consuming but leaves the cron
|
|
144
|
+
* row alive — it's a durable job shared across the fleet. Another
|
|
145
|
+
* container (or this one on restart) will continue servicing it.
|
|
144
146
|
*/
|
|
145
147
|
async disconnect() {
|
|
146
|
-
if (!this.connected)
|
|
147
|
-
return;
|
|
148
|
-
try {
|
|
149
|
-
const connection = (0, db_1.getConnection)();
|
|
150
|
-
await hotmesh_1.Virtual.interrupt({
|
|
151
|
-
topic: CRON_TOPIC,
|
|
152
|
-
connection,
|
|
153
|
-
options: { id: CRON_ID },
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
logger_1.loggerRegistry.warn(`[lt-maintenance] interrupt failed (may not be running): ${err?.message}`);
|
|
158
|
-
}
|
|
159
148
|
this.connected = false;
|
|
160
149
|
}
|
|
161
150
|
/**
|
package/build/start/workers.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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