agents-relay 1.0.177 → 2.0.0
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 +4 -2
- package/dist/adapters.js +26 -2
- package/dist/blockers.js +60 -65
- package/dist/cli-support.js +33 -1
- package/dist/cli.js +72 -86
- package/dist/dashboard-actions.js +13 -35
- package/dist/dashboard-html.js +1 -1
- package/dist/dashboard.js +39 -0
- package/dist/execution-recovery.js +37 -0
- package/dist/graph-runtime.js +1 -0
- package/dist/job-actions.js +53 -0
- package/dist/manual-plan.js +15 -17
- package/dist/planner-runtime.js +135 -0
- package/dist/planner.js +76 -60
- package/dist/pool.js +54 -10
- package/dist/projected-store.js +314 -0
- package/dist/reconciler-review.js +185 -110
- package/dist/reconciler-runtime.js +67 -7
- package/dist/reconciler.js +134 -282
- package/dist/relay-store.js +380 -0
- package/dist/relayd.js +122 -29
- package/dist/runtime-status.js +2 -1
- package/dist/scheduled-jobs.js +40 -31
- package/dist/scheduler.js +19 -18
- package/dist/store.js +1 -1
- package/dist/task-actions.js +139 -0
- package/dist/troubleshooter.js +14 -8
- package/dist/types.js +3 -3
- package/dist/unblock.js +31 -21
- package/dist/workflow/authoritative-unblock-v2.js +20 -0
- package/dist/workflow/blocker-resolution-v2.js +33 -0
- package/dist/workflow/contracts.js +21 -0
- package/dist/workflow/definitions-v2.js +75 -0
- package/dist/workflow/dispatcher.js +56 -0
- package/dist/workflow/entity-creation.js +32 -0
- package/dist/workflow/graph-v2.js +391 -0
- package/dist/workflow/job-reconcile-v2.js +19 -0
- package/dist/workflow/lifecycle.js +82 -4
- package/dist/workflow/planner-orchestration-v2.js +58 -0
- package/dist/workflow/planner-trigger-v2.js +24 -0
- package/dist/workflow/review-v2.js +42 -0
- package/dist/workflow/runtime.js +15 -1
- package/dist/workflow/schedule-cycle-v2.js +25 -0
- package/dist/workflow/scheduled-run-v2.js +30 -0
- package/dist/workflow/task-action-policy.js +19 -0
- package/dist/workflow/task-reconcile.js +6 -6
- package/dist/workflow/task-related-action-v2.js +42 -0
- package/dist/workflow/troubleshooter-v2.js +29 -0
- package/dist/workspace.js +6 -1
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +5 -5
- package/skills/agents-relay/contracts/task-job-contract.md +2 -51
- package/workflow/authoritative-unblock-v2.mmd +15 -0
- package/workflow/blocker-resolution-v2.mmd +73 -0
- package/workflow/job-lifecycle-v2.mmd +70 -0
- package/workflow/job-lifecycle.mmd +2 -2
- package/workflow/planner-orchestration-v2.mmd +65 -0
- package/workflow/planner-trigger-v2.mmd +32 -0
- package/workflow/review-lifecycle-v2.mmd +124 -0
- package/workflow/schedule-cycle-v2.mmd +19 -0
- package/workflow/scheduled-run-v2.mmd +55 -0
- package/workflow/task-lifecycle-v2.mmd +201 -0
- package/workflow/task-lifecycle.mmd +3 -0
- package/workflow/task-related-action-v2.mmd +28 -0
- package/workflow/troubleshooter-v2.mmd +43 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { readdir } from 'node:fs/promises';
|
|
5
|
+
const SCHEMA_VERSION = 4;
|
|
6
|
+
function newerRecord(local, remote) {
|
|
7
|
+
if (!local)
|
|
8
|
+
return structuredClone(remote);
|
|
9
|
+
return remote.updatedAt > local.updatedAt ? structuredClone(remote) : structuredClone(local);
|
|
10
|
+
}
|
|
11
|
+
function mergeRecords(local, remote) {
|
|
12
|
+
const remoteById = new Map(remote.map(item => [item.id, item]));
|
|
13
|
+
const merged = local.map(item => newerRecord(item, remoteById.get(item.id) ?? item));
|
|
14
|
+
const localIds = new Set(local.map(item => item.id));
|
|
15
|
+
for (const item of remote)
|
|
16
|
+
if (!localIds.has(item.id))
|
|
17
|
+
merged.push(structuredClone(item));
|
|
18
|
+
return merged;
|
|
19
|
+
}
|
|
20
|
+
export function mergeInboundJobSnapshot(local, remote) {
|
|
21
|
+
const base = remote.updatedAt > local.updatedAt ? structuredClone(remote) : structuredClone(local);
|
|
22
|
+
base.tasks = mergeRecords(local.tasks, remote.tasks);
|
|
23
|
+
const localPlans = local.plans ?? [];
|
|
24
|
+
const remotePlans = remote.plans ?? [];
|
|
25
|
+
if (localPlans.length > 0 || remotePlans.length > 0) {
|
|
26
|
+
base.plans = mergeRecords(localPlans, remotePlans).sort((a, b) => a.round - b.round);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
delete base.plans;
|
|
30
|
+
}
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
function rowToProjection(row) {
|
|
34
|
+
return {
|
|
35
|
+
id: Number(row.id),
|
|
36
|
+
idempotencyKey: row.idempotency_key,
|
|
37
|
+
coalesceKey: row.coalesce_key,
|
|
38
|
+
operation: row.operation,
|
|
39
|
+
payload: JSON.parse(row.payload_json),
|
|
40
|
+
priority: Number(row.priority),
|
|
41
|
+
attempts: Number(row.attempts),
|
|
42
|
+
nextAttemptAt: row.next_attempt_at,
|
|
43
|
+
lastError: row.last_error,
|
|
44
|
+
createdAt: row.created_at,
|
|
45
|
+
updatedAt: row.updated_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export class SQLiteRelayStore {
|
|
49
|
+
path;
|
|
50
|
+
db;
|
|
51
|
+
constructor(path) {
|
|
52
|
+
this.path = path;
|
|
53
|
+
if (path !== ':memory:')
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
this.db = new DatabaseSync(path);
|
|
56
|
+
this.db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;');
|
|
57
|
+
this.migrate();
|
|
58
|
+
this.db.prepare("UPDATE projection_outbox SET status='pending', updated_at=? WHERE status='processing'")
|
|
59
|
+
.run(new Date().toISOString());
|
|
60
|
+
}
|
|
61
|
+
migrate() {
|
|
62
|
+
this.db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS relay_meta (
|
|
64
|
+
key TEXT PRIMARY KEY,
|
|
65
|
+
value TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS job_snapshot (
|
|
68
|
+
job_id TEXT PRIMARY KEY,
|
|
69
|
+
json TEXT NOT NULL,
|
|
70
|
+
updated_at TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS pull_request_facts (
|
|
73
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
74
|
+
state TEXT NOT NULL CHECK(state IN ('OPEN','MERGED','CLOSED')),
|
|
75
|
+
head_ref TEXT,
|
|
76
|
+
head_sha TEXT,
|
|
77
|
+
observed_at TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS agent_snapshot (
|
|
80
|
+
agent_id TEXT PRIMARY KEY,
|
|
81
|
+
json TEXT NOT NULL,
|
|
82
|
+
updated_at TEXT NOT NULL
|
|
83
|
+
);
|
|
84
|
+
CREATE TABLE IF NOT EXISTS event_receipt (
|
|
85
|
+
event_id TEXT PRIMARY KEY,
|
|
86
|
+
stream TEXT,
|
|
87
|
+
sequence INTEGER,
|
|
88
|
+
received_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE TABLE IF NOT EXISTS projection_outbox (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
93
|
+
coalesce_key TEXT,
|
|
94
|
+
operation TEXT NOT NULL,
|
|
95
|
+
payload_json TEXT NOT NULL,
|
|
96
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','dead')),
|
|
98
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
next_attempt_at TEXT,
|
|
100
|
+
last_error TEXT,
|
|
101
|
+
created_at TEXT NOT NULL,
|
|
102
|
+
updated_at TEXT NOT NULL
|
|
103
|
+
);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS projection_due_idx
|
|
105
|
+
ON projection_outbox(status, next_attempt_at, priority DESC, id);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS projection_coalesce_idx
|
|
107
|
+
ON projection_outbox(coalesce_key, status);
|
|
108
|
+
`);
|
|
109
|
+
const current = this.db.prepare("SELECT value FROM relay_meta WHERE key='schema_version'").get();
|
|
110
|
+
const version = current?.value ? Number(current.value) : 0;
|
|
111
|
+
if (version > SCHEMA_VERSION)
|
|
112
|
+
throw new Error(`Relay store schema ${version} is newer than supported ${SCHEMA_VERSION}`);
|
|
113
|
+
if (version < 4) {
|
|
114
|
+
const schema = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='projection_outbox'").get();
|
|
115
|
+
if (schema?.sql && !schema.sql.includes("'dead'")) {
|
|
116
|
+
this.db.exec(`
|
|
117
|
+
BEGIN IMMEDIATE;
|
|
118
|
+
DROP INDEX IF EXISTS projection_due_idx;
|
|
119
|
+
DROP INDEX IF EXISTS projection_coalesce_idx;
|
|
120
|
+
ALTER TABLE projection_outbox RENAME TO projection_outbox_v3;
|
|
121
|
+
CREATE TABLE projection_outbox (
|
|
122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
124
|
+
coalesce_key TEXT,
|
|
125
|
+
operation TEXT NOT NULL,
|
|
126
|
+
payload_json TEXT NOT NULL,
|
|
127
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
128
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','dead')),
|
|
129
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
130
|
+
next_attempt_at TEXT,
|
|
131
|
+
last_error TEXT,
|
|
132
|
+
created_at TEXT NOT NULL,
|
|
133
|
+
updated_at TEXT NOT NULL
|
|
134
|
+
);
|
|
135
|
+
INSERT INTO projection_outbox(id,idempotency_key,coalesce_key,operation,payload_json,priority,status,attempts,next_attempt_at,last_error,created_at,updated_at)
|
|
136
|
+
SELECT id,idempotency_key,coalesce_key,operation,payload_json,priority,status,attempts,next_attempt_at,last_error,created_at,updated_at FROM projection_outbox_v3;
|
|
137
|
+
DROP TABLE projection_outbox_v3;
|
|
138
|
+
CREATE INDEX projection_due_idx ON projection_outbox(status, next_attempt_at, priority DESC, id);
|
|
139
|
+
CREATE INDEX projection_coalesce_idx ON projection_outbox(coalesce_key, status);
|
|
140
|
+
COMMIT;
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.db.prepare("INSERT INTO relay_meta(key,value) VALUES('schema_version',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
|
145
|
+
.run(String(SCHEMA_VERSION));
|
|
146
|
+
}
|
|
147
|
+
async loadJob(jobId) {
|
|
148
|
+
const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
|
|
149
|
+
return row?.json ? JSON.parse(row.json) : null;
|
|
150
|
+
}
|
|
151
|
+
async listJobs() {
|
|
152
|
+
const rows = this.db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
|
|
153
|
+
return rows.map(row => JSON.parse(row.json));
|
|
154
|
+
}
|
|
155
|
+
async saveJob(job) {
|
|
156
|
+
this.writeJobSnapshot(job, new Date().toISOString());
|
|
157
|
+
}
|
|
158
|
+
async ingestJobSnapshot(job) {
|
|
159
|
+
const now = new Date().toISOString();
|
|
160
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
161
|
+
try {
|
|
162
|
+
const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(job.id);
|
|
163
|
+
const local = row?.json ? JSON.parse(row.json) : null;
|
|
164
|
+
const merged = local ? mergeInboundJobSnapshot(local, job) : structuredClone(job);
|
|
165
|
+
this.writeJobSnapshot(merged, now);
|
|
166
|
+
this.db.exec('COMMIT');
|
|
167
|
+
return structuredClone(merged);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
this.db.exec('ROLLBACK');
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async saveJobWithProjection(job, request) {
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
177
|
+
try {
|
|
178
|
+
this.writeJobSnapshot(job, now);
|
|
179
|
+
this.enqueueProjectionInTransaction(request, now);
|
|
180
|
+
this.db.exec('COMMIT');
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.db.exec('ROLLBACK');
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
writeJobSnapshot(job, now) {
|
|
188
|
+
this.db.prepare(`
|
|
189
|
+
INSERT INTO job_snapshot(job_id,json,updated_at) VALUES(?,?,?)
|
|
190
|
+
ON CONFLICT(job_id) DO UPDATE SET json=excluded.json, updated_at=excluded.updated_at
|
|
191
|
+
`).run(job.id, JSON.stringify(job), now);
|
|
192
|
+
}
|
|
193
|
+
async loadAgents() {
|
|
194
|
+
const observed = this.db.prepare("SELECT value FROM relay_meta WHERE key='agent_registry_observed_at'").get();
|
|
195
|
+
if (!observed?.value)
|
|
196
|
+
return null;
|
|
197
|
+
const rows = this.db.prepare('SELECT json FROM agent_snapshot ORDER BY agent_id').all();
|
|
198
|
+
return rows.map(row => JSON.parse(row.json));
|
|
199
|
+
}
|
|
200
|
+
async saveAgents(agents) {
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
203
|
+
try {
|
|
204
|
+
this.db.prepare('DELETE FROM agent_snapshot').run();
|
|
205
|
+
const insert = this.db.prepare('INSERT INTO agent_snapshot(agent_id,json,updated_at) VALUES(?,?,?)');
|
|
206
|
+
for (const agent of agents)
|
|
207
|
+
insert.run(agent.id, JSON.stringify(agent), now);
|
|
208
|
+
this.markAgentRegistryObserved(now);
|
|
209
|
+
this.db.exec('COMMIT');
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
this.db.exec('ROLLBACK');
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async saveAgentWithProjection(agent, request) {
|
|
217
|
+
const now = new Date().toISOString();
|
|
218
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
219
|
+
try {
|
|
220
|
+
this.writeAgentSnapshot(agent, now);
|
|
221
|
+
this.markAgentRegistryObserved(now);
|
|
222
|
+
this.enqueueProjectionInTransaction(request, now);
|
|
223
|
+
this.db.exec('COMMIT');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
this.db.exec('ROLLBACK');
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
writeAgentSnapshot(agent, now) {
|
|
231
|
+
this.db.prepare(`
|
|
232
|
+
INSERT INTO agent_snapshot(agent_id,json,updated_at) VALUES(?,?,?)
|
|
233
|
+
ON CONFLICT(agent_id) DO UPDATE SET json=excluded.json, updated_at=excluded.updated_at
|
|
234
|
+
`).run(agent.id, JSON.stringify(agent), now);
|
|
235
|
+
}
|
|
236
|
+
markAgentRegistryObserved(now) {
|
|
237
|
+
this.db.prepare(`
|
|
238
|
+
INSERT INTO relay_meta(key,value) VALUES('agent_registry_observed_at',?)
|
|
239
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value
|
|
240
|
+
`).run(now);
|
|
241
|
+
}
|
|
242
|
+
async loadPullRequestFacts() {
|
|
243
|
+
const row = this.db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE id=1').get();
|
|
244
|
+
if (!row?.state || !row.observed_at)
|
|
245
|
+
return null;
|
|
246
|
+
return { state: row.state, head: row.head_ref && row.head_sha ? { ref: row.head_ref, sha: row.head_sha } : null, observedAt: row.observed_at };
|
|
247
|
+
}
|
|
248
|
+
async savePullRequestFacts(facts) {
|
|
249
|
+
this.db.prepare(`
|
|
250
|
+
INSERT INTO pull_request_facts(id,state,head_ref,head_sha,observed_at) VALUES(1,?,?,?,?)
|
|
251
|
+
ON CONFLICT(id) DO UPDATE SET state=excluded.state,head_ref=excluded.head_ref,head_sha=excluded.head_sha,observed_at=excluded.observed_at
|
|
252
|
+
`).run(facts.state, facts.head?.ref ?? null, facts.head?.sha ?? null, facts.observedAt);
|
|
253
|
+
}
|
|
254
|
+
async markEventProcessed(eventId, stream = null, sequence = null) {
|
|
255
|
+
const result = this.db.prepare(`
|
|
256
|
+
INSERT OR IGNORE INTO event_receipt(event_id,stream,sequence,received_at) VALUES(?,?,?,?)
|
|
257
|
+
`).run(eventId, stream, sequence, new Date().toISOString());
|
|
258
|
+
return Number(result.changes) === 1;
|
|
259
|
+
}
|
|
260
|
+
async enqueueProjection(request) {
|
|
261
|
+
const now = new Date().toISOString();
|
|
262
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
263
|
+
try {
|
|
264
|
+
this.enqueueProjectionInTransaction(request, now);
|
|
265
|
+
this.db.exec('COMMIT');
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
this.db.exec('ROLLBACK');
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
enqueueProjectionInTransaction(request, now) {
|
|
273
|
+
if (request.coalesceKey) {
|
|
274
|
+
this.db.prepare(`
|
|
275
|
+
DELETE FROM projection_outbox
|
|
276
|
+
WHERE coalesce_key=? AND status='pending' AND idempotency_key<>?
|
|
277
|
+
`).run(request.coalesceKey, request.idempotencyKey);
|
|
278
|
+
}
|
|
279
|
+
const existing = this.db.prepare('SELECT status FROM projection_outbox WHERE idempotency_key=?').get(request.idempotencyKey);
|
|
280
|
+
// A writer may be applying this key outside the transaction. Never rewrite its
|
|
281
|
+
// payload: its completion must only remove its own row, not a newer mutation.
|
|
282
|
+
const nextId = Number(this.db.prepare('SELECT COALESCE(MAX(id),0)+1 AS next_id FROM projection_outbox').get().next_id ?? 1);
|
|
283
|
+
const idempotencyKey = existing?.status === 'processing' || existing?.status === 'dead'
|
|
284
|
+
? `${request.idempotencyKey}:queued:${nextId}`
|
|
285
|
+
: request.idempotencyKey;
|
|
286
|
+
this.db.prepare(`
|
|
287
|
+
INSERT INTO projection_outbox(
|
|
288
|
+
idempotency_key,coalesce_key,operation,payload_json,priority,status,
|
|
289
|
+
attempts,next_attempt_at,last_error,created_at,updated_at
|
|
290
|
+
) VALUES(?,?,?,?,?,'pending',0,NULL,NULL,?,?)
|
|
291
|
+
ON CONFLICT(idempotency_key) DO UPDATE SET
|
|
292
|
+
coalesce_key=excluded.coalesce_key,
|
|
293
|
+
operation=excluded.operation,
|
|
294
|
+
payload_json=excluded.payload_json,
|
|
295
|
+
priority=MAX(projection_outbox.priority,excluded.priority),
|
|
296
|
+
status='pending',
|
|
297
|
+
next_attempt_at=NULL,
|
|
298
|
+
last_error=NULL,
|
|
299
|
+
updated_at=excluded.updated_at
|
|
300
|
+
`).run(idempotencyKey, request.coalesceKey ?? null, request.operation, JSON.stringify(request.payload), request.priority ?? 0, now, now);
|
|
301
|
+
}
|
|
302
|
+
async claimDueProjections(limit = 50, now = new Date()) {
|
|
303
|
+
const observedAt = now.toISOString();
|
|
304
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
305
|
+
try {
|
|
306
|
+
const rows = this.db.prepare(`
|
|
307
|
+
SELECT id,idempotency_key,coalesce_key,operation,payload_json,priority,
|
|
308
|
+
attempts,next_attempt_at,last_error,created_at,updated_at
|
|
309
|
+
FROM projection_outbox
|
|
310
|
+
WHERE status='pending' AND (next_attempt_at IS NULL OR next_attempt_at<=?)
|
|
311
|
+
ORDER BY priority DESC, id ASC
|
|
312
|
+
LIMIT ?
|
|
313
|
+
`).all(observedAt, Math.max(1, Math.floor(limit)));
|
|
314
|
+
const mark = this.db.prepare("UPDATE projection_outbox SET status='processing', updated_at=? WHERE id=? AND status='pending'");
|
|
315
|
+
const claimed = [];
|
|
316
|
+
for (const row of rows) {
|
|
317
|
+
const result = mark.run(observedAt, row.id);
|
|
318
|
+
if (Number(result.changes) === 1)
|
|
319
|
+
claimed.push({ ...row, updated_at: observedAt });
|
|
320
|
+
}
|
|
321
|
+
this.db.exec('COMMIT');
|
|
322
|
+
return claimed.map(rowToProjection);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.db.exec('ROLLBACK');
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async completeProjection(id) {
|
|
330
|
+
this.db.prepare('DELETE FROM projection_outbox WHERE id=?').run(id);
|
|
331
|
+
}
|
|
332
|
+
async retryProjection(id, error, nextAttemptAt) {
|
|
333
|
+
this.db.prepare(`
|
|
334
|
+
UPDATE projection_outbox
|
|
335
|
+
SET status='pending', attempts=attempts+1, next_attempt_at=?, last_error=?, updated_at=?
|
|
336
|
+
WHERE id=?
|
|
337
|
+
`).run(nextAttemptAt.toISOString(), error, new Date().toISOString(), id);
|
|
338
|
+
}
|
|
339
|
+
async deadLetterProjection(id, error) {
|
|
340
|
+
this.db.prepare(`
|
|
341
|
+
UPDATE projection_outbox
|
|
342
|
+
SET status='dead', attempts=attempts+1, next_attempt_at=NULL, last_error=?, updated_at=?
|
|
343
|
+
WHERE id=?
|
|
344
|
+
`).run(error, new Date().toISOString(), id);
|
|
345
|
+
}
|
|
346
|
+
async pendingProjectionCount() {
|
|
347
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM projection_outbox WHERE status IN ('pending','processing')").get();
|
|
348
|
+
return Number(row?.count ?? 0);
|
|
349
|
+
}
|
|
350
|
+
async terminalProjectionCount() {
|
|
351
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM projection_outbox WHERE status='dead'").get();
|
|
352
|
+
return Number(row?.count ?? 0);
|
|
353
|
+
}
|
|
354
|
+
close() { this.db.close(); }
|
|
355
|
+
}
|
|
356
|
+
export async function discoverLocalRelayStoreSnapshots(directory) {
|
|
357
|
+
let names;
|
|
358
|
+
try {
|
|
359
|
+
names = await readdir(directory);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
const snapshots = [];
|
|
365
|
+
for (const name of names.filter(item => item.endsWith('.db')).sort()) {
|
|
366
|
+
const path = directory + '/' + name;
|
|
367
|
+
let store;
|
|
368
|
+
try {
|
|
369
|
+
store = new SQLiteRelayStore(path);
|
|
370
|
+
snapshots.push({ path, jobs: await store.listJobs(), facts: await store.loadPullRequestFacts() });
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Ignore unrelated/corrupt files so one bad file cannot block daemon startup.
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
store?.close();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return snapshots;
|
|
380
|
+
}
|
package/dist/relayd.js
CHANGED
|
@@ -10,8 +10,12 @@ import { NatsEventBus } from './events.js';
|
|
|
10
10
|
import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
11
11
|
import { Reconciler } from './reconciler.js';
|
|
12
12
|
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestSnapshotGraphql, withGitHubRequestContext } from './store.js';
|
|
13
|
+
import { ProjectedDurableStore } from './projected-store.js';
|
|
14
|
+
import { discoverLocalRelayStoreSnapshots, SQLiteRelayStore } from './relay-store.js';
|
|
13
15
|
import { githubAuthContext } from './github-auth.js';
|
|
14
16
|
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
17
|
+
import { applyJobActivateAction } from './job-actions.js';
|
|
18
|
+
import { applyOrchestratorCompleteAction } from './task-actions.js';
|
|
15
19
|
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
16
20
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
17
21
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
@@ -19,8 +23,14 @@ import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
|
19
23
|
import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
20
24
|
import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
21
25
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
export async function overlayOperationalOverviews(overviews, storeFor) {
|
|
27
|
+
return await Promise.all(overviews.map(async (item) => {
|
|
28
|
+
const store = storeFor(item.job.repository, item.job.prNumber);
|
|
29
|
+
await store.hydrate?.(item.job);
|
|
30
|
+
const job = await store.load(item.job.id);
|
|
31
|
+
return { ...item, job };
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
24
34
|
const PR_REFRESH_COALESCE_MS = 1000;
|
|
25
35
|
export const SCHEDULE_HEARTBEAT_MS = 600_000;
|
|
26
36
|
export function dueScheduledOverviews(overviews, now = new Date()) {
|
|
@@ -40,6 +50,7 @@ Options:
|
|
|
40
50
|
--port PORT Dashboard port (default: 8787)
|
|
41
51
|
--concurrency N Maximum concurrent workers (default: 4)
|
|
42
52
|
--interval MS Recovery interval
|
|
53
|
+
--projection-ms MS GitHub projection flush interval (default: 1000)
|
|
43
54
|
--events nats Enable event wakeups
|
|
44
55
|
|
|
45
56
|
Legacy single-job mode also accepts --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
@@ -109,6 +120,20 @@ export async function runDaemon(argv) {
|
|
|
109
120
|
let cachedOverviews = [];
|
|
110
121
|
const githubReadPaths = { workspaceScans: 0, prRefreshes: 0, prRefreshesByTrigger: {}, prRefreshCoalesced: 0, prRefreshCoalescedByTrigger: {} };
|
|
111
122
|
let dormantProbeIndex = 0;
|
|
123
|
+
const relayStores = new Map();
|
|
124
|
+
const projectionWriters = new Map();
|
|
125
|
+
const runtimeStore = (repository, prNumber, seed) => {
|
|
126
|
+
const key = `${repository}#${prNumber}`;
|
|
127
|
+
let relay = relayStores.get(key);
|
|
128
|
+
if (!relay) {
|
|
129
|
+
relay = new SQLiteRelayStore(join(homedir(), '.agents-relay', 'stores', `${repository.replace('/', '_')}-${prNumber}.db`));
|
|
130
|
+
relayStores.set(key, relay);
|
|
131
|
+
}
|
|
132
|
+
const github = new GitHubStore(client, repository, prNumber, trusted, seed);
|
|
133
|
+
const projected = new ProjectedDurableStore(relay, github, seed ?? null, (relatedRepository, relatedPrNumber) => runtimeStore(relatedRepository, relatedPrNumber));
|
|
134
|
+
projectionWriters.set(key, projected.projection);
|
|
135
|
+
return projected;
|
|
136
|
+
};
|
|
112
137
|
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: new LayaCliDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: async (job) => resolveRepositoryProjectPath(repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot), job.projectPath) });
|
|
113
138
|
if (repositories.length === 0)
|
|
114
139
|
throw new Error(`No Git repositories with GitHub origins found under ${normalized.workspaceRoot ?? defaultWorkspaceRoot()}`);
|
|
@@ -120,7 +145,7 @@ export async function runDaemon(argv) {
|
|
|
120
145
|
cachedOverviews = parsed;
|
|
121
146
|
}
|
|
122
147
|
catch { /* first run or invalid cache */ }
|
|
123
|
-
const refreshAll = async (full = false
|
|
148
|
+
const refreshAll = async (full = false) => {
|
|
124
149
|
if (client.rateLimitStatus().limited)
|
|
125
150
|
return cachedOverviews;
|
|
126
151
|
const scanRepositories = full ? repositories : recoveryScanRepositories(repositories, cachedOverviews, dormantProbeIndex++);
|
|
@@ -130,7 +155,8 @@ export async function runDaemon(argv) {
|
|
|
130
155
|
githubReadPaths.workspaceScans += 1;
|
|
131
156
|
const refreshed = await withGitHubRequestContext(client, 'relayd.workspace-refresh', () => aggregateManagedGitHubJobs(client, scanRepositories, trusted));
|
|
132
157
|
const scanned = new Set(scanRepositories);
|
|
133
|
-
|
|
158
|
+
const merged = dashboardManagedJobs([...cachedOverviews.filter(item => !scanned.has(item.job.repository)), ...refreshed]);
|
|
159
|
+
cachedOverviews = await overlayOperationalOverviews(merged, (repository, prNumber) => runtimeStore(repository, prNumber));
|
|
134
160
|
try {
|
|
135
161
|
await mkdir(cacheDirectory, { recursive: true });
|
|
136
162
|
await writeFile(cacheFile, JSON.stringify(cachedOverviews), 'utf8');
|
|
@@ -139,13 +165,16 @@ export async function runDaemon(argv) {
|
|
|
139
165
|
return cachedOverviews;
|
|
140
166
|
}
|
|
141
167
|
catch (error) {
|
|
142
|
-
if (failOnError)
|
|
143
|
-
throw error;
|
|
144
168
|
process.stderr.write(`warning: workspace job refresh unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
145
169
|
return cachedOverviews;
|
|
146
170
|
}
|
|
147
171
|
};
|
|
148
|
-
const overviews = async (forceRefresh = false, _trigger = 'internal.unknown') =>
|
|
172
|
+
const overviews = async (forceRefresh = false, _trigger = 'internal.unknown') => {
|
|
173
|
+
if (forceRefresh)
|
|
174
|
+
return refreshAll(true);
|
|
175
|
+
cachedOverviews = await overlayOperationalOverviews(cachedOverviews, (repository, prNumber) => runtimeStore(repository, prNumber));
|
|
176
|
+
return cachedOverviews;
|
|
177
|
+
};
|
|
149
178
|
const persistCache = async () => {
|
|
150
179
|
try {
|
|
151
180
|
await mkdir(cacheDirectory, { recursive: true });
|
|
@@ -176,12 +205,14 @@ export async function runDaemon(argv) {
|
|
|
176
205
|
const flight = (async () => {
|
|
177
206
|
githubReadPaths.prRefreshes += 1;
|
|
178
207
|
const snapshot = await withGitHubRequestContext(client, `relayd.pr-refresh:${trigger}`, () => loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trusted));
|
|
208
|
+
const operational = await overlayOperationalOverviews(snapshot.overviews, (repo, pr) => runtimeStore(repo, pr, repo === repository && pr === prNumber ? snapshot.seed : null));
|
|
179
209
|
cachedOverviews = cachedOverviews.filter(item => item.job.repository !== repository || item.job.prNumber !== prNumber);
|
|
180
|
-
cachedOverviews.push(...
|
|
210
|
+
cachedOverviews.push(...operational);
|
|
181
211
|
cachedOverviews = dashboardManagedJobs(cachedOverviews);
|
|
182
212
|
await persistCache();
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
const operationalSnapshot = { ...snapshot, overviews: operational };
|
|
214
|
+
pullRequestRefreshCache.set(key, { snapshot: operationalSnapshot, completedAt: Date.now() });
|
|
215
|
+
return operationalSnapshot;
|
|
185
216
|
})();
|
|
186
217
|
pullRequestRefreshFlights.set(key, flight);
|
|
187
218
|
try {
|
|
@@ -193,15 +224,15 @@ export async function runDaemon(argv) {
|
|
|
193
224
|
}
|
|
194
225
|
};
|
|
195
226
|
const refreshPullRequest = async (repository, prNumber, trigger, options) => (await refreshPullRequestSnapshot(repository, prNumber, trigger, options)).overviews;
|
|
196
|
-
const pool = new RepositoryWorkerPool(client, repositories, trusted, concurrency, makeReconciler, () => client.rateLimitStatus().limited, (repository, prNumber, trigger) => refreshPullRequestSnapshot(repository, prNumber, trigger));
|
|
227
|
+
const pool = new RepositoryWorkerPool(client, repositories, trusted, concurrency, makeReconciler, () => client.rateLimitStatus().limited, (repository, prNumber, trigger) => refreshPullRequestSnapshot(repository, prNumber, trigger), (repository, prNumber, seed) => runtimeStore(repository, prNumber, seed));
|
|
197
228
|
const initial = cachedOverviews;
|
|
198
229
|
const fallback = initial[0]?.job;
|
|
199
|
-
const dashboardStore = fallback ?
|
|
230
|
+
const dashboardStore = fallback ? runtimeStore(fallback.repository, fallback.prNumber) : new InMemoryStore();
|
|
200
231
|
const workspaceJobAction = (action) => async (jobId) => {
|
|
201
232
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
202
233
|
if (!overview)
|
|
203
234
|
throw new Error(`Managed job ${jobId} not found`);
|
|
204
|
-
const store =
|
|
235
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
205
236
|
if (action === 'reconcile') {
|
|
206
237
|
await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId);
|
|
207
238
|
return await store.load(jobId);
|
|
@@ -212,23 +243,56 @@ export async function runDaemon(argv) {
|
|
|
212
243
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
213
244
|
if (!overview)
|
|
214
245
|
throw new Error(`Managed job ${jobId} not found`);
|
|
215
|
-
const store =
|
|
246
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
216
247
|
const result = await dashboardJobSetState(store)(jobId, state);
|
|
217
248
|
await refreshPullRequest(overview.job.repository, overview.job.prNumber, 'dashboard.job-state', { force: true });
|
|
218
249
|
return result;
|
|
219
250
|
};
|
|
251
|
+
const workspaceTaskCreate = async (jobId, task) => {
|
|
252
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
253
|
+
if (!overview)
|
|
254
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
255
|
+
if (task.jobId !== jobId)
|
|
256
|
+
throw new Error(`Task ${task.id} belongs to ${task.jobId}, not ${jobId}`);
|
|
257
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
258
|
+
const job = await store.load(jobId);
|
|
259
|
+
if (['COMPLETED', 'CANCELLED'].includes(job.state))
|
|
260
|
+
throw new Error(`Cannot create task on terminal job ${job.id} (${job.state})`);
|
|
261
|
+
await store.appendTask(task);
|
|
262
|
+
const refreshed = await store.load(jobId);
|
|
263
|
+
if (refreshed.state === 'DRAFT') {
|
|
264
|
+
await applyJobActivateAction(refreshed);
|
|
265
|
+
await store.saveJob(refreshed);
|
|
266
|
+
}
|
|
267
|
+
await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId, 'control.task.create');
|
|
268
|
+
return await store.load(jobId);
|
|
269
|
+
};
|
|
270
|
+
const workspaceTaskComplete = async (jobId, taskId, summary) => {
|
|
271
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
272
|
+
if (!overview)
|
|
273
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
274
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
275
|
+
const job = await store.load(jobId);
|
|
276
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
277
|
+
if (!task)
|
|
278
|
+
throw new Error(`Task ${taskId} not found`);
|
|
279
|
+
await applyOrchestratorCompleteAction(task, summary);
|
|
280
|
+
await store.saveTask(task);
|
|
281
|
+
await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId, 'control.task.complete');
|
|
282
|
+
return await store.load(jobId);
|
|
283
|
+
};
|
|
220
284
|
const workspaceTaskAction = (action) => async (jobId, taskId) => {
|
|
221
285
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
222
286
|
if (!overview)
|
|
223
287
|
throw new Error(`Managed job ${jobId} not found`);
|
|
224
|
-
const store =
|
|
288
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
225
289
|
return await dashboardTaskAction(store, makeReconciler(store, concurrency), action)(jobId, taskId);
|
|
226
290
|
};
|
|
227
291
|
const workspaceTaskSetState = async (jobId, taskId, state) => {
|
|
228
292
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
229
293
|
if (!overview)
|
|
230
294
|
throw new Error(`Managed job ${jobId} not found`);
|
|
231
|
-
const store =
|
|
295
|
+
const store = runtimeStore(overview.job.repository, overview.job.prNumber);
|
|
232
296
|
const result = await dashboardTaskSetState(store)(jobId, taskId, state);
|
|
233
297
|
await refreshPullRequest(overview.job.repository, overview.job.prNumber, 'dashboard.task-state', { force: true });
|
|
234
298
|
return result;
|
|
@@ -273,13 +337,14 @@ export async function runDaemon(argv) {
|
|
|
273
337
|
reconcile: workspaceJobAction('reconcile'),
|
|
274
338
|
cancel: workspaceJobAction('cancel'),
|
|
275
339
|
jobSetState: workspaceJobSetState,
|
|
340
|
+
taskCreate: workspaceTaskCreate,
|
|
276
341
|
taskRetry: workspaceTaskAction('retry'),
|
|
277
342
|
taskCancel: workspaceTaskAction('cancel'),
|
|
343
|
+
taskComplete: workspaceTaskComplete,
|
|
278
344
|
taskSetState: workspaceTaskSetState,
|
|
279
345
|
});
|
|
346
|
+
await once(server, 'listening');
|
|
280
347
|
const reconcileSafely = async () => {
|
|
281
|
-
if (client.rateLimitStatus().limited)
|
|
282
|
-
return;
|
|
283
348
|
try {
|
|
284
349
|
await pool.reconcile();
|
|
285
350
|
}
|
|
@@ -288,37 +353,65 @@ export async function runDaemon(argv) {
|
|
|
288
353
|
}
|
|
289
354
|
};
|
|
290
355
|
const scheduleHeartbeatSafely = async () => {
|
|
291
|
-
if (client.rateLimitStatus().limited)
|
|
292
|
-
return;
|
|
293
356
|
const now = new Date();
|
|
294
357
|
for (const item of dueScheduledOverviews(cachedOverviews, now)) {
|
|
295
|
-
const repository = item.job.repository;
|
|
296
358
|
try {
|
|
297
|
-
await
|
|
359
|
+
await pool.scheduleTickSignal(bus, item.job, now);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
process.stderr.write(`warning: scheduled heartbeat unavailable for ${item.job.repository}#${item.job.prNumber}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const flushProjectionsSafely = async () => {
|
|
367
|
+
for (const writer of projectionWriters.values()) {
|
|
368
|
+
try {
|
|
369
|
+
await writer.flushOnce();
|
|
298
370
|
}
|
|
299
371
|
catch (error) {
|
|
300
|
-
process.stderr.write(`warning:
|
|
372
|
+
process.stderr.write(`warning: GitHub projection flush unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
301
373
|
}
|
|
302
374
|
}
|
|
303
375
|
};
|
|
304
376
|
const watchdogDefault = webhook ? SERVICE_DEFAULTS.webhookWatchdogMs : SERVICE_DEFAULTS.watchdogMs;
|
|
305
377
|
let interval;
|
|
306
378
|
let scheduleInterval;
|
|
379
|
+
let projectionInterval;
|
|
307
380
|
const stop = () => { if (interval)
|
|
308
381
|
clearInterval(interval); if (scheduleInterval)
|
|
309
|
-
clearInterval(scheduleInterval);
|
|
382
|
+
clearInterval(scheduleInterval); if (projectionInterval)
|
|
383
|
+
clearInterval(projectionInterval); void stopWatch().finally(() => { for (const store of relayStores.values())
|
|
384
|
+
store.close(); server.close(() => process.exit(0)); }); };
|
|
310
385
|
process.once('SIGINT', stop);
|
|
311
386
|
process.once('SIGTERM', stop);
|
|
312
|
-
await
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
387
|
+
const localSnapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
|
|
388
|
+
const knownRepositories = new Set(repositories);
|
|
389
|
+
const localOverviews = localSnapshots.flatMap(snapshot => {
|
|
390
|
+
if (!snapshot.facts)
|
|
391
|
+
return [];
|
|
392
|
+
return snapshot.jobs.filter(job => knownRepositories.has(job.repository)).map(job => ({
|
|
393
|
+
job, githubState: snapshot.facts.state, draft: false, mergedAt: null, closedAt: null, publication: null,
|
|
394
|
+
}));
|
|
395
|
+
});
|
|
396
|
+
const byJob = new Map();
|
|
397
|
+
for (const item of cachedOverviews)
|
|
398
|
+
byJob.set(item.job.repository + '#' + item.job.prNumber + ':' + item.job.id, item);
|
|
399
|
+
for (const item of localOverviews)
|
|
400
|
+
byJob.set(item.job.repository + '#' + item.job.prNumber + ':' + item.job.id, item);
|
|
401
|
+
cachedOverviews = dashboardManagedJobs([...byJob.values()]);
|
|
402
|
+
await pool.reconcileKnown(cachedOverviews);
|
|
316
403
|
await scheduleHeartbeatSafely();
|
|
317
|
-
|
|
404
|
+
await flushProjectionsSafely();
|
|
405
|
+
interval = setInterval(() => void reconcileSafely().finally(flushProjectionsSafely), Number(value(argv, '--interval', String(watchdogDefault))));
|
|
318
406
|
interval.unref();
|
|
407
|
+
projectionInterval = setInterval(() => void flushProjectionsSafely(), Number(value(argv, '--projection-ms', String(SERVICE_DEFAULTS.projectionFlushMs))));
|
|
408
|
+
projectionInterval.unref();
|
|
319
409
|
scheduleInterval = setInterval(() => void scheduleHeartbeatSafely(), SCHEDULE_HEARTBEAT_MS);
|
|
320
410
|
scheduleInterval.unref();
|
|
321
411
|
console.log('Dashboard listening; repository worker pool active');
|
|
412
|
+
void refreshAll(true)
|
|
413
|
+
.then(items => pool.reconcileKnown(items))
|
|
414
|
+
.catch(error => process.stderr.write('warning: startup remote refresh unavailable: ' + (error instanceof Error ? error.message : String(error)) + '\n'));
|
|
322
415
|
}
|
|
323
416
|
if (isEntrypoint(import.meta.url))
|
|
324
417
|
runDaemon(process.argv.slice(2)).catch(error => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
|