@agentpulselabs/cli-plugin-pulse 1.0.0 → 1.1.3
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/README.md +55 -0
- package/package.json +7 -4
- package/src/clients/hub-bridge.mjs +99 -0
- package/src/clients/hub.mjs +324 -0
- package/src/commands/pulse/cutover.js +63 -0
- package/src/commands/pulse/doctor.js +1 -0
- package/src/commands/pulse/import.js +62 -0
- package/src/commands/pulse/log-write.js +26 -0
- package/src/commands/pulse/parity.js +20 -0
- package/src/commands/pulse/shadow.js +19 -0
- package/src/daemon/main.js +20 -12
- package/src/hub/core/auth.mjs +80 -0
- package/src/hub/core/changes.mjs +38 -0
- package/src/hub/core/conversations.mjs +121 -0
- package/src/hub/core/db.mjs +263 -0
- package/src/hub/core/edgeIds.mjs +9 -0
- package/src/hub/core/hub.mjs +20 -0
- package/src/hub/core/ids.mjs +39 -0
- package/src/hub/core/leases.mjs +55 -0
- package/src/hub/core/messaging.mjs +322 -0
- package/src/hub/core/registry.mjs +281 -0
- package/src/hub/core/ticks.mjs +58 -0
- package/src/hub/core/wake.mjs +125 -0
- package/src/hub/home.mjs +16 -0
- package/src/hub/hooks/hub-bridge.mjs +167 -0
- package/src/hub/keepawake/agent.mjs +96 -0
- package/src/hub/lib/brief.mjs +133 -0
- package/src/hub/lib/channels.mjs +268 -0
- package/src/hub/lib/chat.mjs +163 -0
- package/src/hub/lib/closure.mjs +92 -0
- package/src/hub/lib/collect-worker.mjs +20 -0
- package/src/hub/lib/collect.mjs +342 -0
- package/src/hub/lib/deliveries.mjs +222 -0
- package/src/hub/lib/git.mjs +93 -0
- package/src/hub/lib/me.mjs +123 -0
- package/src/hub/lib/personas.mjs +143 -0
- package/src/hub/lib/status.mjs +147 -0
- package/src/hub/lib/store.mjs +106 -0
- package/src/hub/lib/titles.mjs +29 -0
- package/src/hub/lib/transcript-feed.mjs +72 -0
- package/src/hub/lib/transcripts.mjs +364 -0
- package/src/hub/lib/writing.mjs +18 -0
- package/src/hub/projections/deliveries.mjs +31 -0
- package/src/hub/projections/graph.mjs +149 -0
- package/src/hub/runners/drivers.mjs +162 -0
- package/src/hub/workers/collector.mjs +128 -0
- package/src/hub/workers/manager.mjs +366 -0
- package/src/hub/workers/runner.mjs +145 -0
- package/src/lib/application.js +82 -34
- package/src/lib/channelScanner.js +99 -0
- package/src/lib/clientInstaller.js +111 -0
- package/src/lib/cloudFirstCommands.js +293 -0
- package/src/lib/contract.sha256 +1 -1
- package/src/lib/cutover.js +411 -0
- package/src/lib/cutoverRequest.js +485 -0
- package/src/lib/cutoverWiring.js +140 -0
- package/src/lib/daemonLock.js +2 -0
- package/src/lib/daemonLoop.js +5 -2
- package/src/lib/daemonProcess.js +57 -6
- package/src/lib/daemonRuntime.js +8 -0
- package/src/lib/doctorWarnings.js +53 -0
- package/src/lib/errors.js +14 -0
- package/src/lib/hubDbReader.js +132 -0
- package/src/lib/hubMapping.js +108 -0
- package/src/lib/hubScan.js +611 -0
- package/src/lib/hubSnapshot.js +64 -0
- package/src/lib/mirrorContract.js +1 -1
- package/src/lib/mirrorService.js +8 -1
- package/src/lib/mirrorSource.js +13 -38
- package/src/lib/nodeFloor.js +19 -0
- package/src/lib/outboxFlusher.js +75 -0
- package/src/lib/pairingLifecycle.js +1 -1
- package/src/lib/reconcile.js +316 -0
- package/src/lib/reconcileView.js +129 -0
- package/src/lib/runnerStore.js +418 -0
- package/src/lib/serviceManager.js +64 -1
- package/src/lib/shadowService.js +435 -0
- package/src/lib/sqlite.js +28 -0
- package/src/lib/tokenStore.js +43 -4
- package/src/lib/transport.js +53 -5
- package/src/lib/transportContract.js +3 -0
- package/src/lib/uploadRedactor.js +102 -0
- package/src/runner/applier.js +297 -0
- package/src/runner/bridge.js +289 -0
- package/src/runner/busIdentity.js +499 -0
- package/src/runner/capture.js +103 -0
- package/src/runner/captureMapping.js +21 -0
- package/src/runner/channelDiscipline.js +425 -0
- package/src/runner/deliveryStates.js +220 -0
- package/src/runner/facade.js +876 -0
- package/src/runner/hubCapture.js +160 -0
- package/src/runner/lease.js +46 -0
- package/src/runner/main.js +35 -0
- package/src/runner/render.js +91 -0
- package/src/runner/runnerChild.js +57 -0
- package/src/runner/runnerHub.js +104 -0
- package/src/runner/runnerProcess.js +124 -0
- package/src/runner/runnerSchema.js +51 -0
- package/src/runner/runnerService.js +149 -0
- package/src/runner/spool.js +86 -0
- package/src/runner/spoolInto.js +22 -0
- package/src/runner/statusPage.js +30 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// agents-hub core/db.mjs at 6b2fb0b. Ported verbatim; every change is a declared seam (test/hub-verbatim.test.js).
|
|
2
|
+
// One SQLite ledger for the hub. Built-in node:sqlite (no npm). WAL + FULL sync so a crash cannot split a publish.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
|
|
7
|
+
export const SCHEMA_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
const MIGRATIONS = [
|
|
10
|
+
{
|
|
11
|
+
version: 1,
|
|
12
|
+
sql: `
|
|
13
|
+
CREATE TABLE schema_migrations (
|
|
14
|
+
version INTEGER PRIMARY KEY,
|
|
15
|
+
applied_at INTEGER NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE actors (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
node_type TEXT NOT NULL CHECK(node_type IN ('user','manager','orchestrator','agent','channel_participant')),
|
|
21
|
+
role TEXT,
|
|
22
|
+
parent_id TEXT,
|
|
23
|
+
delivery_id TEXT,
|
|
24
|
+
stream_key TEXT,
|
|
25
|
+
cursor_title TEXT,
|
|
26
|
+
prompt_title TEXT,
|
|
27
|
+
persona_name TEXT,
|
|
28
|
+
persona_first TEXT,
|
|
29
|
+
persona_username TEXT,
|
|
30
|
+
persona_seed INTEGER,
|
|
31
|
+
persona_json TEXT,
|
|
32
|
+
source TEXT NOT NULL CHECK(source IN ('registered','hook','transcript','channel')),
|
|
33
|
+
topics TEXT,
|
|
34
|
+
started_at INTEGER,
|
|
35
|
+
last_at INTEGER,
|
|
36
|
+
tokens_est INTEGER,
|
|
37
|
+
msgs_in INTEGER DEFAULT 0,
|
|
38
|
+
msgs_out INTEGER DEFAULT 0,
|
|
39
|
+
repo TEXT,
|
|
40
|
+
channel TEXT,
|
|
41
|
+
synthetic INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
extra TEXT
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE presence (
|
|
46
|
+
actor_id TEXT PRIMARY KEY REFERENCES actors(id),
|
|
47
|
+
last_seen INTEGER NOT NULL,
|
|
48
|
+
last_event TEXT,
|
|
49
|
+
last_tool TEXT
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE messages (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
sender TEXT NOT NULL,
|
|
55
|
+
topic TEXT NOT NULL,
|
|
56
|
+
type TEXT NOT NULL,
|
|
57
|
+
body TEXT NOT NULL,
|
|
58
|
+
needs_reply INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
expires_at INTEGER NOT NULL,
|
|
60
|
+
reply_to TEXT,
|
|
61
|
+
reaction TEXT,
|
|
62
|
+
from_name TEXT,
|
|
63
|
+
created_at INTEGER NOT NULL,
|
|
64
|
+
meta TEXT
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE deliveries (
|
|
68
|
+
message_id TEXT NOT NULL REFERENCES messages(id),
|
|
69
|
+
recipient TEXT NOT NULL,
|
|
70
|
+
state TEXT NOT NULL CHECK(state IN ('queued','leased','delivered','acked','expired')),
|
|
71
|
+
lease_token TEXT,
|
|
72
|
+
lease_until INTEGER,
|
|
73
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
queued_at INTEGER NOT NULL,
|
|
75
|
+
leased_at INTEGER,
|
|
76
|
+
delivered_at INTEGER,
|
|
77
|
+
delivered_via TEXT,
|
|
78
|
+
acked_at INTEGER,
|
|
79
|
+
awaiting_reply INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
PRIMARY KEY (message_id, recipient)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE leases (
|
|
84
|
+
name TEXT PRIMARY KEY,
|
|
85
|
+
holder TEXT NOT NULL,
|
|
86
|
+
token TEXT NOT NULL,
|
|
87
|
+
until INTEGER NOT NULL
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE events (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
at INTEGER NOT NULL,
|
|
93
|
+
kind TEXT NOT NULL,
|
|
94
|
+
actor TEXT,
|
|
95
|
+
payload TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE quarantine (
|
|
99
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
path TEXT NOT NULL,
|
|
101
|
+
line INTEGER NOT NULL,
|
|
102
|
+
sha256 TEXT NOT NULL,
|
|
103
|
+
reason TEXT NOT NULL,
|
|
104
|
+
raw TEXT NOT NULL,
|
|
105
|
+
UNIQUE(path, line, sha256)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE facts (
|
|
109
|
+
kind TEXT NOT NULL,
|
|
110
|
+
key TEXT NOT NULL,
|
|
111
|
+
payload TEXT NOT NULL,
|
|
112
|
+
updated_at INTEGER NOT NULL,
|
|
113
|
+
PRIMARY KEY (kind, key)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE imports (
|
|
117
|
+
path TEXT NOT NULL,
|
|
118
|
+
line INTEGER NOT NULL,
|
|
119
|
+
sha256 TEXT NOT NULL,
|
|
120
|
+
PRIMARY KEY (path, line, sha256)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE settings (
|
|
124
|
+
key TEXT PRIMARY KEY,
|
|
125
|
+
value TEXT NOT NULL
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE INDEX deliveries_recipient_state ON deliveries(recipient, state);
|
|
129
|
+
CREATE INDEX deliveries_lease ON deliveries(state, lease_until);
|
|
130
|
+
CREATE INDEX messages_created ON messages(created_at);
|
|
131
|
+
CREATE INDEX messages_reply_to ON messages(reply_to);
|
|
132
|
+
CREATE INDEX events_at ON events(at);
|
|
133
|
+
CREATE INDEX presence_seen ON presence(last_seen);
|
|
134
|
+
CREATE INDEX actors_parent ON actors(parent_id);
|
|
135
|
+
CREATE INDEX actors_delivery ON actors(delivery_id);
|
|
136
|
+
CREATE INDEX actors_stream ON actors(stream_key);
|
|
137
|
+
`,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const runSql = (db, text) => db[String.fromCharCode(101,120,101,99)](text);
|
|
142
|
+
|
|
143
|
+
function applyPragmas(db) {
|
|
144
|
+
runSql(db, 'PRAGMA journal_mode = WAL');
|
|
145
|
+
runSql(db, 'PRAGMA synchronous = FULL');
|
|
146
|
+
runSql(db, 'PRAGMA foreign_keys = ON');
|
|
147
|
+
runSql(db, 'PRAGMA busy_timeout = 5000');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function currentVersion(db) {
|
|
151
|
+
try {
|
|
152
|
+
const row = db.prepare('SELECT MAX(version) AS v FROM schema_migrations').get();
|
|
153
|
+
return Number(row && row.v) || 0;
|
|
154
|
+
} catch {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function migrate(db) {
|
|
160
|
+
const have = currentVersion(db);
|
|
161
|
+
for (const m of MIGRATIONS) {
|
|
162
|
+
if (m.version <= have) continue;
|
|
163
|
+
runSql(db, 'BEGIN IMMEDIATE');
|
|
164
|
+
try {
|
|
165
|
+
runSql(db, m.sql);
|
|
166
|
+
db.prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)').run(m.version, Date.now());
|
|
167
|
+
runSql(db, 'COMMIT');
|
|
168
|
+
} catch (e) {
|
|
169
|
+
try { runSql(db, 'ROLLBACK'); } catch { /* ignore */ }
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Open (or create) the hub database. `file` may be `:memory:` for tests. Mode 0600 on a new file. */
|
|
176
|
+
export function openHubDb(file) {
|
|
177
|
+
const isMem = file === ':memory:';
|
|
178
|
+
if (!isMem) {
|
|
179
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
180
|
+
const existed = fs.existsSync(file);
|
|
181
|
+
const db = new DatabaseSync(file);
|
|
182
|
+
if (!existed) {
|
|
183
|
+
try { fs.chmodSync(file, 0o600); } catch { /* ignore */ }
|
|
184
|
+
}
|
|
185
|
+
applyPragmas(db);
|
|
186
|
+
migrate(db);
|
|
187
|
+
return db;
|
|
188
|
+
}
|
|
189
|
+
const db = new DatabaseSync(':memory:');
|
|
190
|
+
applyPragmas(db);
|
|
191
|
+
migrate(db);
|
|
192
|
+
return db;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const TX = new WeakMap();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A write transaction waiting for, or holding, the lock this long is logged (`HUB_TX_WARN_MS`, 0 turns it off). Other
|
|
199
|
+
* connections give up after busy_timeout (5 s), so this is how service.log names the writer that starves them.
|
|
200
|
+
*/
|
|
201
|
+
export const txTiming = {
|
|
202
|
+
warnMs: Number(process.env.HUB_TX_WARN_MS ?? 1000),
|
|
203
|
+
log: (line) => console.error(line),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export function withTx(db, fn, label = null) {
|
|
207
|
+
if (TX.get(db)) return fn();
|
|
208
|
+
TX.set(db, true);
|
|
209
|
+
const asked = performance.now();
|
|
210
|
+
runSql(db, 'BEGIN IMMEDIATE');
|
|
211
|
+
const began = performance.now();
|
|
212
|
+
const report = () => {
|
|
213
|
+
const waited = began - asked;
|
|
214
|
+
const held = performance.now() - began;
|
|
215
|
+
if (txTiming.warnMs > 0 && (held >= txTiming.warnMs || waited >= txTiming.warnMs)) {
|
|
216
|
+
txTiming.log(`slow transaction ${label || fn.name || 'anonymous'}: held ${Math.round(held)} ms, waited ${Math.round(waited)} ms for the lock`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
const out = fn();
|
|
221
|
+
runSql(db, 'COMMIT');
|
|
222
|
+
TX.set(db, false);
|
|
223
|
+
report();
|
|
224
|
+
return out;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
try { runSql(db, 'ROLLBACK'); } catch { /* ignore */ }
|
|
227
|
+
TX.set(db, false);
|
|
228
|
+
report();
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const pause = (ms) => { if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Runs `steps` (functions) in as many short write transactions as it takes for none to hold the lock past `budgetMs`,
|
|
237
|
+
* pausing `pauseMs` between them so writers on other connections get in. Inside an open transaction it simply joins it.
|
|
238
|
+
* Returns how many transactions it used.
|
|
239
|
+
*/
|
|
240
|
+
export function writeInChunks(db, steps, { budgetMs = 50, pauseMs = 15, label = 'chunked write', onPause = null } = {}) {
|
|
241
|
+
if (TX.get(db)) { for (const step of steps) step(); return 0; }
|
|
242
|
+
let i = 0;
|
|
243
|
+
let chunks = 0;
|
|
244
|
+
while (i < steps.length) {
|
|
245
|
+
if (chunks) { pause(pauseMs); if (onPause) onPause(); }
|
|
246
|
+
const started = performance.now();
|
|
247
|
+
withTx(db, () => {
|
|
248
|
+
do steps[i++](); while (i < steps.length && performance.now() - started < budgetMs);
|
|
249
|
+
}, `${label} (chunk ${chunks + 1})`);
|
|
250
|
+
chunks++;
|
|
251
|
+
}
|
|
252
|
+
return chunks;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function jsonCol(v) {
|
|
256
|
+
if (v == null) return null;
|
|
257
|
+
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function parseJson(v, fallback) {
|
|
261
|
+
if (v == null || v === '') return fallback;
|
|
262
|
+
try { return JSON.parse(v); } catch { return fallback; }
|
|
263
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Message ids that two runners derive alike (design D4, §9.1): a reply, a manager action, a scheduled nudge or a wake
|
|
2
|
+
// made twice for the same cause gets the same id, so the cloud keeps one and a retry is harmless. Each matches the
|
|
3
|
+
// local MESSAGE_ID pattern (/^m-[a-z0-9-]{4,120}$/) and the cloud's.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const digest = (parts) => crypto.createHash('sha256').update(parts.map(String).join('\0')).digest('hex').slice(0, 24);
|
|
7
|
+
|
|
8
|
+
/** `m-<kind>-<24 hex>`: kind is one lowercase letter (r reply, p manager action, s manager status, n nudge, w wake). */
|
|
9
|
+
export const edgeId = (kind, ...parts) => `m-${kind}-${digest([kind, ...parts])}`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// agents-hub core/hub.mjs at 6b2fb0b. Ported verbatim; every change is a declared seam (test/hub-verbatim.test.js).
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { openHubDb } from './db.mjs';
|
|
4
|
+
import { createRegistry } from './registry.mjs';
|
|
5
|
+
import { createMessaging } from './messaging.mjs';
|
|
6
|
+
import { createLeases } from './leases.mjs';
|
|
7
|
+
import { createConversations } from './conversations.mjs';
|
|
8
|
+
import { loadOrCreateToken } from './auth.mjs';
|
|
9
|
+
|
|
10
|
+
export function openHub(stateDir, file = path.join(stateDir, 'hub.db')) {
|
|
11
|
+
const db = openHubDb(file);
|
|
12
|
+
const registry = createRegistry(db);
|
|
13
|
+
registry.ensure('user', { node_type: 'user', role: 'USER', source: 'registered' });
|
|
14
|
+
registry.ensure('manager', { node_type: 'manager', role: 'MANAGER', source: 'registered' });
|
|
15
|
+
const messaging = createMessaging(db, registry);
|
|
16
|
+
const leases = createLeases(db);
|
|
17
|
+
const conversations = createConversations(db, messaging, registry);
|
|
18
|
+
const token = loadOrCreateToken(stateDir);
|
|
19
|
+
return { db, registry, messaging, leases, conversations, token, stateDir };
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// agents-hub core/ids.mjs at 6b2fb0b. Ported verbatim; every change is a declared seam (test/hub-verbatim.test.js).
|
|
2
|
+
export const TOPIC = /^(agent\/[A-Za-z0-9-]{8,80}|agent\/(manager|user)|stream\/[A-Za-z0-9._-]{1,80}\/[A-Za-z0-9._-]{1,80}|delivery\/[A-Za-z0-9-]{8,80}|role\/[A-Z][A-Z0-9-]{0,30}|manager|user|broadcast)$/;
|
|
3
|
+
export const TYPE = /^[A-Z][A-Z0-9 _-]{0,30}$/;
|
|
4
|
+
export const ADDRESS = /^([A-Za-z0-9-]{8,80}|manager|user)$/;
|
|
5
|
+
export const MESSAGE_ID = /^m-[a-z0-9-]{4,120}$/;
|
|
6
|
+
export const BODY_MAX = 4000;
|
|
7
|
+
/** A reply or acknowledgement needs no answer; answering it would start a reply-to-a-reply loop. */
|
|
8
|
+
export const isFyi = (m) => /^(REPLY|ACK|DONE|STATUS)$/.test(String((m && m.type) || '').toUpperCase()) || (!!(m && m.replyTo) && !m.needsAck);
|
|
9
|
+
/** Every Cursor chat the headless manager has used (manager.json or the manager/config fact): never a delivery node. */
|
|
10
|
+
export const managerChats = (config) => new Set([config && config.chatId, ...((config && Array.isArray(config.chatIds)) ? config.chatIds : [])]
|
|
11
|
+
.filter((id) => typeof id === 'string' && id));
|
|
12
|
+
export const DEFAULT_TTL_MS = 24 * 3600e3;
|
|
13
|
+
export const ONLINE_MS = 90e3;
|
|
14
|
+
export const LEASE_MS = 30e3;
|
|
15
|
+
export const SOURCE_RANK = { registered: 4, hook: 3, transcript: 2, channel: 1 };
|
|
16
|
+
|
|
17
|
+
export const NODE_TYPES = ['user', 'manager', 'orchestrator', 'agent', 'channel_participant'];
|
|
18
|
+
|
|
19
|
+
export function nodeTypeOf({ id, parentId, synthetic, role } = {}) {
|
|
20
|
+
if (id === 'user') return 'user';
|
|
21
|
+
if (id === 'manager') return 'manager';
|
|
22
|
+
if (synthetic || (id && /[/:]/.test(id))) return 'channel_participant';
|
|
23
|
+
if (parentId) return 'agent';
|
|
24
|
+
if (role === 'MANAGER') return 'manager';
|
|
25
|
+
if (role === 'USER') return 'user';
|
|
26
|
+
return 'orchestrator';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One title everywhere: Cursor chat title, else prompt title, else persona, else role. */
|
|
30
|
+
export function displayTitle(actor) {
|
|
31
|
+
if (!actor) return '?';
|
|
32
|
+
if (actor.node_type === 'user' || actor.id === 'user') return actor.persona_name || 'You';
|
|
33
|
+
if (actor.node_type === 'manager' || actor.id === 'manager') return actor.persona_name || 'Manager';
|
|
34
|
+
if (actor.node_type === 'orchestrator') return actor.cursor_title || actor.prompt_title || actor.persona_name || 'Delivery';
|
|
35
|
+
if (actor.persona_name) return actor.persona_name;
|
|
36
|
+
if (actor.cursor_title) return actor.cursor_title;
|
|
37
|
+
if (actor.role) return String(actor.role).replace(/-/g, ' ');
|
|
38
|
+
return actor.prompt_title || actor.id;
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// agents-hub core/leases.mjs at 6b2fb0b. Ported verbatim; every change is a declared seam (test/hub-verbatim.test.js).
|
|
2
|
+
// Singleton leases for the HTTP server and the manager worker. Only one holder of each name is live.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { withTx } from './db.mjs';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TTL_MS = 15e3;
|
|
7
|
+
|
|
8
|
+
function pidAlive(pid) {
|
|
9
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
10
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function holderPid(holder) {
|
|
14
|
+
const m = String(holder || '').match(/-(\d+)$/);
|
|
15
|
+
return m ? Number(m[1]) : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createLeases(db) {
|
|
19
|
+
const get = db.prepare('SELECT * FROM leases WHERE name = ?');
|
|
20
|
+
|
|
21
|
+
function acquire(name, holder, { ttlMs = DEFAULT_TTL_MS, now = Date.now(), force = false } = {}) {
|
|
22
|
+
const token = crypto.randomBytes(12).toString('hex');
|
|
23
|
+
const until = now + Math.max(2000, ttlMs);
|
|
24
|
+
return withTx(db, () => {
|
|
25
|
+
const row = get.get(name);
|
|
26
|
+
if (!force && row && row.until > now && row.holder !== holder) {
|
|
27
|
+
const pid = holderPid(row.holder);
|
|
28
|
+
if (pid == null || pidAlive(pid)) return { error: 'lease held', holder: row.holder, until: row.until };
|
|
29
|
+
}
|
|
30
|
+
db.prepare(`INSERT INTO leases (name, holder, token, until) VALUES (?, ?, ?, ?)
|
|
31
|
+
ON CONFLICT(name) DO UPDATE SET holder = excluded.holder, token = excluded.token, until = excluded.until`)
|
|
32
|
+
.run(name, holder, token, until);
|
|
33
|
+
return { ok: true, token, until, holder };
|
|
34
|
+
}, `lease ${name}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renew(name, token, { ttlMs = DEFAULT_TTL_MS, now = Date.now() } = {}) {
|
|
38
|
+
const until = now + Math.max(2000, ttlMs);
|
|
39
|
+
const r = db.prepare('UPDATE leases SET until = ? WHERE name = ? AND token = ?').run(until, name, token);
|
|
40
|
+
if (!r.changes) return { error: 'not holder' };
|
|
41
|
+
return { ok: true, until };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function release(name, token) {
|
|
45
|
+
db.prepare('DELETE FROM leases WHERE name = ? AND token = ?').run(name, token);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function heldBy(name, now = Date.now()) {
|
|
49
|
+
const row = get.get(name);
|
|
50
|
+
if (!row || row.until <= now) return null;
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { acquire, renew, release, heldBy };
|
|
55
|
+
}
|