@agentpulselabs/cli-plugin-pulse 0.2.0 → 1.1.2
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 +115 -32
- package/package.json +14 -19
- package/src/clients/hub-bridge.mjs +99 -0
- package/src/clients/hub.mjs +324 -0
- package/src/commands/pulse/cutover.js +39 -0
- package/src/commands/pulse/doctor.js +15 -0
- package/src/commands/pulse/hub-write.js +26 -0
- package/src/commands/pulse/import.js +62 -0
- package/src/commands/pulse/install.js +14 -0
- package/src/commands/pulse/log-write.js +26 -0
- package/src/commands/pulse/login.js +20 -0
- package/src/commands/pulse/mirror.js +27 -0
- package/src/commands/pulse/pair.js +38 -0
- package/src/commands/pulse/parity.js +20 -0
- package/src/commands/pulse/restart.js +14 -0
- package/src/commands/pulse/shadow.js +19 -0
- package/src/commands/pulse/start.js +6 -18
- package/src/commands/pulse/status.js +6 -6
- package/src/commands/pulse/stop.js +24 -0
- package/src/daemon/main.js +43 -0
- 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/BaseCommand.js +23 -23
- package/src/lib/agentSessions.js +279 -0
- package/src/lib/application.js +704 -0
- package/src/lib/backoff.js +8 -0
- package/src/lib/budgets.js +85 -0
- package/src/lib/channelScanner.js +99 -0
- package/src/lib/cliDefaults.js +20 -0
- package/src/lib/clientInstaller.js +111 -0
- package/src/lib/cloudAuth.js +189 -0
- package/src/lib/cloudFirstCommands.js +293 -0
- package/src/lib/contract.sha256 +2 -0
- package/src/lib/cutover.js +376 -0
- package/src/lib/cutoverWiring.js +137 -0
- package/src/lib/daemonComposition.js +338 -0
- package/src/lib/daemonLock.js +57 -0
- package/src/lib/daemonLoop.js +72 -0
- package/src/lib/daemonProcess.js +380 -0
- package/src/lib/daemonRuntime.js +828 -0
- package/src/lib/doctorWarnings.js +53 -0
- package/src/lib/drivers.js +280 -0
- package/src/lib/errors.js +33 -0
- package/src/lib/graphWindows.js +149 -0
- package/src/lib/http.js +84 -0
- package/src/lib/hubActions.js +74 -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/identity.js +102 -0
- package/src/lib/intents.js +590 -0
- package/src/lib/ledger.js +535 -0
- package/src/lib/lifecycle.js +123 -0
- package/src/lib/localHub.js +217 -0
- package/src/lib/localHubWriter.js +69 -0
- package/src/lib/mirror-contract.json +4299 -0
- package/src/lib/mirrorContract.js +513 -0
- package/src/lib/mirrorDaemon.js +127 -0
- package/src/lib/mirrorService.js +365 -0
- package/src/lib/mirrorSource.js +430 -0
- package/src/lib/mirrorSync.js +353 -0
- package/src/lib/nodeFloor.js +19 -0
- package/src/lib/oauthLogin.js +132 -0
- package/src/lib/outboxFlusher.js +75 -0
- package/src/lib/ownerLock.js +99 -0
- package/src/lib/pairedConfig.js +40 -0
- package/src/lib/pairingLifecycle.js +179 -0
- package/src/lib/pathRedaction.js +89 -0
- package/src/lib/planContract.js +141 -0
- package/src/lib/priorityLane.js +80 -0
- package/src/lib/processOwnership.js +252 -0
- package/src/lib/processSupervisor.js +376 -0
- package/src/lib/reconcile.js +316 -0
- package/src/lib/reconcileView.js +129 -0
- package/src/lib/redaction.js +128 -0
- package/src/lib/rootPolicy.js +372 -0
- package/src/lib/rulePacks.js +162 -0
- package/src/lib/runnerStore.js +418 -0
- package/src/lib/secretStore.js +169 -0
- package/src/lib/selfRestart.js +261 -0
- package/src/lib/serviceManager.js +419 -0
- package/src/lib/shadowService.js +435 -0
- package/src/lib/sqlite.js +28 -0
- package/src/lib/storage.js +131 -0
- package/src/lib/subprocess.js +45 -0
- package/src/lib/tokenStore.js +136 -0
- package/src/lib/transport.js +334 -0
- package/src/lib/transportContract.js +40 -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 +140 -0
- package/src/runner/spool.js +86 -0
- package/src/runner/spoolInto.js +22 -0
- package/src/runner/statusPage.js +30 -0
- package/LICENSE +0 -21
- package/src/commands/pulse/gate.js +0 -25
- package/src/commands/pulse/next.js +0 -14
- package/src/commands/pulse/submit.js +0 -21
- package/src/commands/pulse/verify.js +0 -21
- package/src/runPulse.js +0 -58
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { openDatabase } from './sqlite.js';
|
|
3
|
+
|
|
4
|
+
const parseJson = (text, fallback) => {
|
|
5
|
+
if (text === null || text === undefined || text === '') return fallback;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(text);
|
|
8
|
+
} catch {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** An actor row in the local registry's shape (agents-hub core/registry.mjs rowActor at 37a0b2f, without `title`). */
|
|
14
|
+
export function rowActor(r) {
|
|
15
|
+
return {
|
|
16
|
+
id: r.id,
|
|
17
|
+
node_type: r.node_type,
|
|
18
|
+
role: r.role,
|
|
19
|
+
parent_id: r.parent_id,
|
|
20
|
+
delivery_id: r.delivery_id,
|
|
21
|
+
stream_key: r.stream_key,
|
|
22
|
+
cursor_title: r.cursor_title,
|
|
23
|
+
prompt_title: r.prompt_title,
|
|
24
|
+
persona_name: r.persona_name,
|
|
25
|
+
persona_first: r.persona_first,
|
|
26
|
+
persona_username: r.persona_username,
|
|
27
|
+
persona_seed: r.persona_seed,
|
|
28
|
+
persona: parseJson(r.persona_json, null),
|
|
29
|
+
source: r.source,
|
|
30
|
+
topics: parseJson(r.topics, []),
|
|
31
|
+
started_at: r.started_at,
|
|
32
|
+
last_at: r.last_at,
|
|
33
|
+
tokens_est: r.tokens_est,
|
|
34
|
+
msgs_in: r.msgs_in,
|
|
35
|
+
msgs_out: r.msgs_out,
|
|
36
|
+
repo: r.repo,
|
|
37
|
+
channel: r.channel,
|
|
38
|
+
synthetic: !!r.synthetic,
|
|
39
|
+
extra: parseJson(r.extra, null),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const plain = (row) => ({ ...row });
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read-only access to the local hub's `state/hub.db`. Each `read(fn)` opens the file read-only (no `immutable`, no
|
|
47
|
+
* pragma that writes), runs every query of one pass inside a single short read transaction so recipients and their
|
|
48
|
+
* messages come from the same snapshot, and closes the handle, so the hub's writers and WAL checkpoints never wait on
|
|
49
|
+
* a reader across an await. `fn` must be synchronous.
|
|
50
|
+
*/
|
|
51
|
+
export class HubDbReader {
|
|
52
|
+
constructor({ hubDir, file = path.join(hubDir, 'state', 'hub.db'), busyMs = 2_000 }) {
|
|
53
|
+
this.file = file;
|
|
54
|
+
this.busyMs = busyMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
read(fn) {
|
|
58
|
+
const db = openDatabase(this.file, { readOnly: true });
|
|
59
|
+
try {
|
|
60
|
+
db.prepare(`PRAGMA busy_timeout = ${Number(this.busyMs)}`).run();
|
|
61
|
+
db.prepare('BEGIN').run();
|
|
62
|
+
try {
|
|
63
|
+
return fn(new HubSnapshot(db));
|
|
64
|
+
} finally {
|
|
65
|
+
db.prepare('COMMIT').run();
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
db.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The queries of one read transaction. */
|
|
74
|
+
export class HubSnapshot {
|
|
75
|
+
constructor(db) {
|
|
76
|
+
this.db = db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every actor in table order (the order the local registry lists them). */
|
|
80
|
+
actors() {
|
|
81
|
+
return this.db.prepare('SELECT * FROM actors ORDER BY rowid').all().map(rowActor);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
presence() {
|
|
85
|
+
return this.db.prepare('SELECT actor_id, last_seen, last_event FROM presence ORDER BY rowid').all().map(plain);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Messages created at or after `sinceMs`, in (created_at, rowid) order: rowid breaks ties the way the hub inserted them. */
|
|
89
|
+
messagesSince(sinceMs) {
|
|
90
|
+
return this.db.prepare('SELECT rowid AS _rowid, * FROM messages WHERE created_at >= ? ORDER BY created_at, rowid').all(sinceMs)
|
|
91
|
+
.map((row) => ({ ...row, meta: parseJson(row.meta, {}) }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
messageIdsSince(sinceMs) {
|
|
95
|
+
return new Set(this.db.prepare('SELECT id FROM messages WHERE created_at >= ?').all(sinceMs).map((row) => row.id));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
messageExists(id) {
|
|
99
|
+
return Boolean(this.db.prepare('SELECT 1 AS found FROM messages WHERE id = ?').get(id));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Recipient rows of these messages, in table order. */
|
|
103
|
+
recipients(messageIds) {
|
|
104
|
+
const out = new Map();
|
|
105
|
+
const statement = this.db.prepare(
|
|
106
|
+
'SELECT message_id, recipient, state, attempts, delivered_at, delivered_via, acked_at FROM deliveries WHERE message_id = ? ORDER BY rowid',
|
|
107
|
+
);
|
|
108
|
+
for (const id of messageIds) out.set(id, statement.all(id).map(plain));
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One recipient row with its message's expiry, or null when either is gone. */
|
|
113
|
+
receipt(messageId, recipient) {
|
|
114
|
+
const row = this.db.prepare(
|
|
115
|
+
`SELECT d.message_id, d.recipient, d.state, d.attempts, d.delivered_at, d.delivered_via, d.acked_at, m.expires_at, m.topic
|
|
116
|
+
FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
117
|
+
WHERE d.message_id = ? AND d.recipient = ?`,
|
|
118
|
+
).get(messageId, recipient);
|
|
119
|
+
return row ? plain(row) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
eventsAfter(id) {
|
|
123
|
+
return this.db.prepare('SELECT id, at, kind, actor, payload FROM events WHERE id > ? ORDER BY id').all(id)
|
|
124
|
+
.map((row) => ({ ...row, payload: parseJson(row.payload, {}) }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Facts of these kinds with their payload text as stored (parsed by the caller only when it changed). */
|
|
128
|
+
facts(kinds) {
|
|
129
|
+
const statement = this.db.prepare('SELECT kind, key, payload, updated_at FROM facts WHERE kind = ? ORDER BY key');
|
|
130
|
+
return kinds.flatMap((kind) => statement.all(kind).map(plain));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { pathRejected } from './mirrorContract.js';
|
|
3
|
+
import { canonicalJson } from './planContract.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The local hub's rows as Pulse commands (design §11 table, cf-phase2 PLAN v2), as pure functions: the import, the
|
|
7
|
+
* shadow feed and anything that captures the same rows later (Phase 3) produce the same keys and payloads. Redaction
|
|
8
|
+
* is not done here: callers pass rows through the UploadRedactor, and content keys are taken from redacted payloads.
|
|
9
|
+
*/
|
|
10
|
+
const KEY_PART = /^[A-Za-z0-9._:/@-]{1,200}$/;
|
|
11
|
+
const PERSONA_FIELDS = ['name', 'first', 'username', 'seed', 'personality'];
|
|
12
|
+
|
|
13
|
+
const sha = (text) => createHash('sha256').update(text).digest('hex');
|
|
14
|
+
export const without = (object, ...keys) => Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
|
|
15
|
+
|
|
16
|
+
/** `<label>:<part>` when the part is key-safe, else `<label>:h:<sha40>`, so a key never carries a space, path or secret. */
|
|
17
|
+
export function keyPart(label, part) {
|
|
18
|
+
return KEY_PART.test(String(part)) && !pathRejected(String(part)) ? `${label}:${part}` : `${label}:h:${sha(String(part)).slice(0, 40)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The content key of a state entity: the redacted payload without its `at`, digested. */
|
|
22
|
+
export function contentSha(redactedPayload) {
|
|
23
|
+
return sha(canonicalJson(without(redactedPayload, 'at'))).slice(0, 12);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function actorPayload(actor) {
|
|
27
|
+
return { id: actor.id, actor, at: Math.floor(Number(actor.last_at) || 0) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function messagePayload(message, recipients) {
|
|
31
|
+
return {
|
|
32
|
+
id: message.id, from: message.sender, fromName: message.from_name ?? null, topic: message.topic, type: message.type,
|
|
33
|
+
body: message.body, to: [...new Set(recipients.map((row) => row.recipient))], needsAck: Boolean(message.needs_reply),
|
|
34
|
+
expiresAt: Number(message.expires_at), replyTo: message.reply_to ?? null, reaction: message.reaction ?? null,
|
|
35
|
+
meta: message.meta ?? {}, at: Number(message.created_at),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The receipts a recipient row's current state implies (`leased` is `queued`: leases stay on the machine). */
|
|
40
|
+
export function receiptEvents(row, topic, expiresAt) {
|
|
41
|
+
const base = { messageId: row.message_id, recipient: row.recipient };
|
|
42
|
+
const events = [];
|
|
43
|
+
const state = row.state === 'leased' ? 'queued' : row.state;
|
|
44
|
+
if (row.delivered_at !== null && row.delivered_at !== undefined) {
|
|
45
|
+
events.push({ type: 'receipt.delivered', key: `rcpt:${row.message_id}:${row.recipient}:delivered`, payload: { ...base, at: Number(row.delivered_at), ...(row.delivered_via ? { via: row.delivered_via } : {}) } });
|
|
46
|
+
}
|
|
47
|
+
if (state === 'acked') {
|
|
48
|
+
events.push({ type: 'receipt.acked', key: `rcpt:${row.message_id}:${row.recipient}:acked`, payload: { ...base, at: Number(row.acked_at ?? row.delivered_at ?? row.queued_at ?? expiresAt) } });
|
|
49
|
+
}
|
|
50
|
+
if (state === 'expired') {
|
|
51
|
+
events.push({ type: 'receipt.expired', key: `rcpt:${row.message_id}:${row.recipient}:expired`, payload: { ...base, at: Number(expiresAt) } });
|
|
52
|
+
}
|
|
53
|
+
return { state, events: events.map((event) => ({ ...event, group: `bus:${topic}` })) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function eventPayload(event) {
|
|
57
|
+
return { kind: event.kind, actor: event.actor ?? null, data: event.payload ?? {}, at: Number(event.at) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function personaPayload(id, persona) {
|
|
61
|
+
const value = Object.fromEntries(PERSONA_FIELDS.filter((field) => persona?.[field] !== undefined && persona?.[field] !== null).map((field) => [field, persona[field]]));
|
|
62
|
+
return { actorId: id, persona: value };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The commands of one fact row, each with its entity and key prefix (content keyed): a delivery travels without its
|
|
67
|
+
* streams, which go as stream facts of their own so a large delivery fits a command.
|
|
68
|
+
*/
|
|
69
|
+
export function factDescriptors(row, fact) {
|
|
70
|
+
const entity = `fact:${row.kind}:${row.key}`;
|
|
71
|
+
const prefix = keyPart(`fact:${row.kind}`, row.key);
|
|
72
|
+
const at = Number.isFinite(fact.at) ? fact.at : Number(row.updated_at);
|
|
73
|
+
const one = (type, payload) => ({ type, entity, prefix, group: entity, payload });
|
|
74
|
+
switch (row.kind) {
|
|
75
|
+
case 'delivery': {
|
|
76
|
+
const streams = Array.isArray(fact.streams) ? fact.streams : [];
|
|
77
|
+
const delivery = { ...without(fact, 'streams'), streamKeys: streams.map((stream) => stream.key) };
|
|
78
|
+
return [
|
|
79
|
+
one('delivery.updated', { id: row.key, delivery, at: Number(row.updated_at) }),
|
|
80
|
+
...streams.map((stream) => ({
|
|
81
|
+
type: 'stream.updated', entity: `fact:stream:${stream.key}`, prefix: keyPart('fact:stream', stream.key), group: `fact:stream:${stream.key}`,
|
|
82
|
+
payload: { key: stream.key, deliveryId: row.key, stream: without(stream, 'deliveryId'), at: Number(row.updated_at) },
|
|
83
|
+
})),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
case 'agent-row':
|
|
87
|
+
return [one('actor.row_updated', { id: row.key, row: fact, at: Number(row.updated_at) })];
|
|
88
|
+
case 'request':
|
|
89
|
+
return [one('request.created', { id: row.key, request: fact, at })];
|
|
90
|
+
case 'accepted':
|
|
91
|
+
return [one('delivery.accepted', { id: row.key, at })];
|
|
92
|
+
case 'settings':
|
|
93
|
+
return [one('settings.changed', { key: row.key, value: fact, at: Number(row.updated_at) })];
|
|
94
|
+
case 'status_override':
|
|
95
|
+
return [one('status.overridden', { key: row.key, override: fact, at })];
|
|
96
|
+
default:
|
|
97
|
+
throw new TypeError(`hub mapping: fact kind ${row.kind} has no command`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A task-channel post: its key from its own block's digest and occurrence, never from its inferred time. */
|
|
102
|
+
export function postKey(channelKey, digestId) {
|
|
103
|
+
return `post:${channelKey}:${digestId}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function postPayload(channel, post, body, truncated) {
|
|
107
|
+
return { channel, author: post.role, role: post.role, type: post.type, body, at: post.at, ...(truncated ? { truncated } : {}) };
|
|
108
|
+
}
|