@atolis-hq/wake 0.2.54 → 0.2.55
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/dist/src/adapters/fs/state-store.js +243 -10
- package/dist/src/adapters/github/github-pull-request-activity-source.js +103 -79
- package/dist/src/adapters/http/ui-assets.js +13 -0
- package/dist/src/adapters/http/ui-data.js +33 -10
- package/dist/src/core/stale-run-reconciler.js +14 -5
- package/dist/src/core/tick-runner.js +31 -7
- package/dist/src/lib/paths.js +2 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { access, appendFile, mkdir, readFile, readdir, rename } from 'node:fs/promises';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { validateResourceIndex } from './resource-index.js';
|
|
4
5
|
import { parseEventEnvelope, parseIssueStateRecord, parseLedger, parseRunRecord, parseSourceStateRecord, } from '../../domain/schema.js';
|
|
5
6
|
import { isTerminalStage } from '../../domain/stages.js';
|
|
6
7
|
import { appendJsonLine, readJsonFile, writeJsonFile } from '../../lib/json-file.js';
|
|
8
|
+
import { acquireFileLock } from '../../lib/lock.js';
|
|
7
9
|
import { createWakePaths } from '../../lib/paths.js';
|
|
8
10
|
import { isMissingPathError, stateHealthIssue, StateHealthError, throwIfUnhealthy, } from '../../lib/state-health.js';
|
|
11
|
+
const runSummaryIndexLockTimeoutMs = 5_000;
|
|
12
|
+
const runSummaryIndexLockPollMs = 10;
|
|
9
13
|
async function readIssueStateFile(file) {
|
|
10
14
|
try {
|
|
11
15
|
return parseIssueStateRecord(await readJsonFile(file));
|
|
@@ -24,9 +28,116 @@ async function readIssueStateFile(file) {
|
|
|
24
28
|
]);
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
|
|
31
|
+
// The board/runs/metrics UI list many run records at once purely for their
|
|
32
|
+
// small fields (status, timing, routing) - metadata.stdout/stderr/raw and
|
|
33
|
+
// runtimeEvents are captured agent output that can run into single-digit MB
|
|
34
|
+
// per run, and holding all of them in memory for a full-history listing is
|
|
35
|
+
// what took the UI process OOM. Stripped only for bulk-list reads; single-run
|
|
36
|
+
// reads (readRunRecord) keep the full record since reconciliation code
|
|
37
|
+
// spreads a record's metadata back into a rewrite (see stale-run-reconciler).
|
|
38
|
+
function stripHeavyRunRecordFields(record) {
|
|
39
|
+
if (record.metadata === undefined && record.runtimeEvents === undefined) {
|
|
40
|
+
return record;
|
|
41
|
+
}
|
|
42
|
+
const { runtimeEvents: _runtimeEvents, metadata, ...rest } = record;
|
|
43
|
+
if (metadata === undefined) {
|
|
44
|
+
return rest;
|
|
45
|
+
}
|
|
46
|
+
const { stdout: _stdout, stderr: _stderr, raw: _raw, ...restMetadata } = metadata;
|
|
47
|
+
return { ...rest, metadata: restMetadata };
|
|
48
|
+
}
|
|
49
|
+
function parseRunRecordSummaryIndex(input, date) {
|
|
50
|
+
if (input === null || typeof input !== 'object') {
|
|
51
|
+
throw new Error('Run summary index must be an object');
|
|
52
|
+
}
|
|
53
|
+
const record = input;
|
|
54
|
+
if (record.schemaVersion !== 1) {
|
|
55
|
+
throw new Error('Run summary index schemaVersion must be 1');
|
|
56
|
+
}
|
|
57
|
+
if (record.date !== date) {
|
|
58
|
+
throw new Error(`Run summary index date must be ${date}`);
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(record.entries)) {
|
|
61
|
+
throw new Error('Run summary index entries must be an array');
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
date,
|
|
66
|
+
entries: record.entries.map((entry) => parseRunRecord(entry)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function withRunSummaryIndexLock(paths, date, operation) {
|
|
70
|
+
const deadline = Date.now() + runSummaryIndexLockTimeoutMs;
|
|
71
|
+
while (true) {
|
|
72
|
+
const lock = await acquireFileLock(paths.runDateIndexLockFile(date), {
|
|
73
|
+
staleAfterMs: runSummaryIndexLockTimeoutMs,
|
|
74
|
+
});
|
|
75
|
+
if (lock.acquired) {
|
|
76
|
+
try {
|
|
77
|
+
return await operation();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await lock.release();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() >= deadline) {
|
|
84
|
+
throw new Error(`Timed out acquiring run summary index lock for ${date}`);
|
|
85
|
+
}
|
|
86
|
+
await delay(runSummaryIndexLockPollMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function readRunSummaryIndex(paths, date) {
|
|
90
|
+
return parseRunRecordSummaryIndex(await readJsonFile(paths.runDateIndexFile(date)), date);
|
|
91
|
+
}
|
|
92
|
+
async function writeRunSummaryIndex(paths, date, entries) {
|
|
93
|
+
await writeJsonFile(paths.runDateIndexFile(date), {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
date,
|
|
96
|
+
entries: entries.sort((left, right) => left.startedAt.localeCompare(right.startedAt)),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function countRunDateBucketFiles(paths, date) {
|
|
100
|
+
return (await readdir(join(paths.dataRoot, 'runs', 'by-date', date), { withFileTypes: true }).catch((error) => {
|
|
101
|
+
if (isMissingPathError(error)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
})).filter((file) => file.isFile() && file.name.endsWith('.json') && file.name !== 'index.json')
|
|
106
|
+
.length;
|
|
107
|
+
}
|
|
108
|
+
async function rebuildRunSummaryIndexForDate(paths, date) {
|
|
109
|
+
const recordsById = new Map();
|
|
110
|
+
const bucketFiles = (await readdir(join(paths.dataRoot, 'runs', 'by-date', date)).catch(() => []))
|
|
111
|
+
.filter((file) => file.endsWith('.json') && file !== 'index.json')
|
|
112
|
+
.sort();
|
|
113
|
+
for (const file of bucketFiles) {
|
|
114
|
+
const record = await readRunRecordFile(join(paths.dataRoot, 'runs', 'by-date', date, file), {
|
|
115
|
+
summarize: true,
|
|
116
|
+
});
|
|
117
|
+
if (record !== null) {
|
|
118
|
+
recordsById.set(record.runId, record);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const entries = [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
122
|
+
await writeRunSummaryIndex(paths, date, entries);
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
async function upsertRunSummaryIndexEntry(paths, record) {
|
|
126
|
+
const date = record.startedAt.slice(0, 10);
|
|
127
|
+
const summary = stripHeavyRunRecordFields(record);
|
|
128
|
+
await withRunSummaryIndexLock(paths, date, async () => {
|
|
129
|
+
const entries = await readRunSummaryIndex(paths, date)
|
|
130
|
+
.then((index) => index.entries)
|
|
131
|
+
.catch(() => []);
|
|
132
|
+
const nextById = new Map(entries.map((entry) => [entry.runId, entry]));
|
|
133
|
+
nextById.set(summary.runId, summary);
|
|
134
|
+
await writeRunSummaryIndex(paths, date, [...nextById.values()]);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async function readRunRecordFile(file, options) {
|
|
28
138
|
try {
|
|
29
|
-
|
|
139
|
+
const record = parseRunRecord(await readJsonFile(file));
|
|
140
|
+
return options?.summarize === true ? stripHeavyRunRecordFields(record) : record;
|
|
30
141
|
}
|
|
31
142
|
catch {
|
|
32
143
|
return null;
|
|
@@ -167,6 +278,44 @@ async function collectEventIssues(paths) {
|
|
|
167
278
|
}
|
|
168
279
|
return issues;
|
|
169
280
|
}
|
|
281
|
+
async function collectRunSummaryIndexIssues(paths) {
|
|
282
|
+
const issues = [];
|
|
283
|
+
const byDateRoot = join(paths.dataRoot, 'runs', 'by-date');
|
|
284
|
+
const dateDirs = await readdir(byDateRoot, { withFileTypes: true }).catch((error) => {
|
|
285
|
+
if (isMissingPathError(error)) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
});
|
|
290
|
+
for (const entry of dateDirs) {
|
|
291
|
+
if (!entry.isDirectory()) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const date = entry.name;
|
|
295
|
+
const runFileCount = await countRunDateBucketFiles(paths, date);
|
|
296
|
+
const indexFile = paths.runDateIndexFile(date);
|
|
297
|
+
try {
|
|
298
|
+
const index = await readRunSummaryIndex(paths, date);
|
|
299
|
+
if (index.entries.length !== runFileCount) {
|
|
300
|
+
issues.push(stateHealthIssue({
|
|
301
|
+
surface: 'runs',
|
|
302
|
+
kind: 'incomplete',
|
|
303
|
+
path: indexFile,
|
|
304
|
+
message: `Run summary index has ${index.entries.length} entries but ${runFileCount} run files exist for ${date}`,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
issues.push(stateHealthIssue({
|
|
310
|
+
surface: 'runs',
|
|
311
|
+
kind: isMissingPathError(error) ? 'incomplete' : 'corrupted',
|
|
312
|
+
path: indexFile,
|
|
313
|
+
message: error instanceof Error ? error.message : String(error),
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return issues;
|
|
318
|
+
}
|
|
170
319
|
function issueArchiveAgeDate(item) {
|
|
171
320
|
const stageChangedAt = item.wake.stageHistory.at(-1)?.changedAt;
|
|
172
321
|
return ([stageChangedAt, item.wake.syncedAt, item.issue.updatedAt]
|
|
@@ -181,13 +330,13 @@ function shouldArchiveIssueState(item, options) {
|
|
|
181
330
|
const ageMs = options.now.getTime() - Date.parse(issueArchiveAgeDate(item));
|
|
182
331
|
return Number.isFinite(ageMs) && ageMs > options.archiveFreshnessDays * 24 * 60 * 60 * 1000;
|
|
183
332
|
}
|
|
184
|
-
|
|
333
|
+
async function listRunRecordsImpl(wakeRoot, options) {
|
|
185
334
|
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
186
335
|
const recordsById = new Map();
|
|
187
336
|
try {
|
|
188
337
|
const files = (await readdir(runsRoot)).filter((file) => file.endsWith('.json')).sort();
|
|
189
338
|
for (const file of files) {
|
|
190
|
-
const record = await readRunRecordFile(join(runsRoot, file));
|
|
339
|
+
const record = await readRunRecordFile(join(runsRoot, file), options);
|
|
191
340
|
if (record !== null) {
|
|
192
341
|
recordsById.set(record.runId, record);
|
|
193
342
|
}
|
|
@@ -204,7 +353,7 @@ export async function listRunRecords(wakeRoot) {
|
|
|
204
353
|
.filter((file) => file.endsWith('.json'))
|
|
205
354
|
.sort();
|
|
206
355
|
for (const file of files) {
|
|
207
|
-
const record = await readRunRecordFile(join(byDateRoot, dateDir, file));
|
|
356
|
+
const record = await readRunRecordFile(join(byDateRoot, dateDir, file), options);
|
|
208
357
|
if (record !== null) {
|
|
209
358
|
recordsById.set(record.runId, record);
|
|
210
359
|
}
|
|
@@ -216,14 +365,38 @@ export async function listRunRecords(wakeRoot) {
|
|
|
216
365
|
}
|
|
217
366
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
218
367
|
}
|
|
219
|
-
async function
|
|
368
|
+
export async function listRunRecords(wakeRoot) {
|
|
369
|
+
return listRunRecordsImpl(wakeRoot);
|
|
370
|
+
}
|
|
371
|
+
// Same records as listRunRecords, but with metadata.stdout/stderr/raw and
|
|
372
|
+
// runtimeEvents stripped per-file as they're read - for callers (board/runs/
|
|
373
|
+
// metrics UI) that list every run and only need the small fields. See
|
|
374
|
+
// stripHeavyRunRecordFields for why this can't just be a post-hoc .map() over
|
|
375
|
+
// listRunRecords: that would still hold every full record in memory at once.
|
|
376
|
+
export async function listRunRecordSummaries(wakeRoot) {
|
|
377
|
+
const paths = createWakePaths(wakeRoot);
|
|
378
|
+
const byDateRoot = join(paths.dataRoot, 'runs', 'by-date');
|
|
379
|
+
const dateDirs = (await readdir(byDateRoot).catch(() => [])).sort();
|
|
380
|
+
if (dateDirs.length === 0) {
|
|
381
|
+
return listRunRecordsImpl(wakeRoot, { summarize: true });
|
|
382
|
+
}
|
|
383
|
+
const recordsById = new Map();
|
|
384
|
+
for (const dateDir of dateDirs) {
|
|
385
|
+
const records = await listRunRecordSummariesForDate(wakeRoot, dateDir);
|
|
386
|
+
for (const record of records) {
|
|
387
|
+
recordsById.set(record.runId, record);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
391
|
+
}
|
|
392
|
+
async function listRunRecordsForDateImpl(wakeRoot, date, options) {
|
|
220
393
|
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
221
394
|
const recordsById = new Map();
|
|
222
395
|
const bucketFiles = (await readdir(join(runsRoot, 'by-date', date)).catch(() => []))
|
|
223
396
|
.filter((file) => file.endsWith('.json'))
|
|
224
397
|
.sort();
|
|
225
398
|
for (const file of bucketFiles) {
|
|
226
|
-
const record = await readRunRecordFile(join(runsRoot, 'by-date', date, file));
|
|
399
|
+
const record = await readRunRecordFile(join(runsRoot, 'by-date', date, file), options);
|
|
227
400
|
if (record !== null) {
|
|
228
401
|
recordsById.set(record.runId, record);
|
|
229
402
|
}
|
|
@@ -233,7 +406,7 @@ async function listRunRecordsForDate(wakeRoot, date) {
|
|
|
233
406
|
.filter((file) => file.endsWith('.json'))
|
|
234
407
|
.sort();
|
|
235
408
|
for (const file of legacyFiles) {
|
|
236
|
-
const record = await readRunRecordFile(join(runsRoot, file));
|
|
409
|
+
const record = await readRunRecordFile(join(runsRoot, file), options);
|
|
237
410
|
if (record?.startedAt.slice(0, 10) === date) {
|
|
238
411
|
recordsById.set(record.runId, record);
|
|
239
412
|
}
|
|
@@ -241,6 +414,42 @@ async function listRunRecordsForDate(wakeRoot, date) {
|
|
|
241
414
|
}
|
|
242
415
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
243
416
|
}
|
|
417
|
+
async function listRunRecordsForDate(wakeRoot, date) {
|
|
418
|
+
return listRunRecordsForDateImpl(wakeRoot, date);
|
|
419
|
+
}
|
|
420
|
+
async function listRunRecordSummariesForDate(wakeRoot, date) {
|
|
421
|
+
const paths = createWakePaths(wakeRoot);
|
|
422
|
+
try {
|
|
423
|
+
const index = await readRunSummaryIndex(paths, date);
|
|
424
|
+
const runFileCount = await countRunDateBucketFiles(paths, date);
|
|
425
|
+
if (index.entries.length === runFileCount && (index.entries.length > 0 || runFileCount > 0)) {
|
|
426
|
+
return index.entries;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Rebuild below under the per-date lock.
|
|
431
|
+
}
|
|
432
|
+
const rebuilt = await withRunSummaryIndexLock(paths, date, async () => {
|
|
433
|
+
const runFileCount = await countRunDateBucketFiles(paths, date);
|
|
434
|
+
if (runFileCount === 0) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const index = await readRunSummaryIndex(paths, date);
|
|
439
|
+
if (index.entries.length === runFileCount) {
|
|
440
|
+
return index.entries;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Rebuild below.
|
|
445
|
+
}
|
|
446
|
+
return rebuildRunSummaryIndexForDate(paths, date);
|
|
447
|
+
});
|
|
448
|
+
if (rebuilt !== null) {
|
|
449
|
+
return rebuilt;
|
|
450
|
+
}
|
|
451
|
+
return listRunRecordsForDateImpl(wakeRoot, date, { summarize: true });
|
|
452
|
+
}
|
|
244
453
|
async function listRecentRunRecords(wakeRoot, limit) {
|
|
245
454
|
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
246
455
|
const recordsById = new Map();
|
|
@@ -297,6 +506,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
297
506
|
const parsed = parseRunRecord(record);
|
|
298
507
|
await writeJsonFile(paths.runFile(parsed.runId), parsed);
|
|
299
508
|
await writeJsonFile(paths.runDateFile(parsed.startedAt.slice(0, 10), parsed.runId), parsed);
|
|
509
|
+
await upsertRunSummaryIndexEntry(paths, parsed);
|
|
300
510
|
return parsed;
|
|
301
511
|
},
|
|
302
512
|
async updateRunRecordIf(runId, input) {
|
|
@@ -316,16 +526,38 @@ export function createStateStore({ wakeRoot }) {
|
|
|
316
526
|
return parseRunRecord(await readJsonFile(paths.runFile(runId)));
|
|
317
527
|
}
|
|
318
528
|
catch {
|
|
319
|
-
|
|
320
|
-
|
|
529
|
+
// buildBoard calls this once per work item (readRunRecord per
|
|
530
|
+
// lastRunId), so a missing flat file used to mean a full,
|
|
531
|
+
// unstripped listRecentRunRecords(500) scan - every heavy
|
|
532
|
+
// stdout/raw payload in the 500 most recent runs, parsed again for
|
|
533
|
+
// every item on the board. Scan summaries to locate the record's
|
|
534
|
+
// date bucket instead, then do one targeted full read of just that
|
|
535
|
+
// file so the caller still gets full fidelity.
|
|
536
|
+
const summaries = await listRunRecordSummaries(wakeRoot);
|
|
537
|
+
const match = summaries.find((record) => record.runId === runId);
|
|
538
|
+
if (match === undefined) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
return parseRunRecord(await readJsonFile(paths.runDateFile(match.startedAt.slice(0, 10), runId)));
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return match;
|
|
546
|
+
}
|
|
321
547
|
}
|
|
322
548
|
},
|
|
323
549
|
async listRunRecords() {
|
|
324
550
|
return listRunRecords(wakeRoot);
|
|
325
551
|
},
|
|
552
|
+
async listRunRecordSummaries() {
|
|
553
|
+
return listRunRecordSummaries(wakeRoot);
|
|
554
|
+
},
|
|
326
555
|
async listRunRecordsForDate(date) {
|
|
327
556
|
return listRunRecordsForDate(wakeRoot, date);
|
|
328
557
|
},
|
|
558
|
+
async listRunRecordSummariesForDate(date) {
|
|
559
|
+
return listRunRecordSummariesForDate(wakeRoot, date);
|
|
560
|
+
},
|
|
329
561
|
async listRecentRunRecords(limit = 10) {
|
|
330
562
|
return listRecentRunRecords(wakeRoot, limit);
|
|
331
563
|
},
|
|
@@ -511,6 +743,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
511
743
|
const issues = [
|
|
512
744
|
...(await collectEventIssues(paths)),
|
|
513
745
|
...(await collectIssueStateIssues(paths)),
|
|
746
|
+
...(await collectRunSummaryIndexIssues(paths)),
|
|
514
747
|
...(await validateResourceIndex(paths)),
|
|
515
748
|
];
|
|
516
749
|
return { healthy: issues.length === 0, issues };
|
|
@@ -82,6 +82,53 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
82
82
|
rootId: Number(rootIdStr),
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
+
function prCommentPublishedEvent(input) {
|
|
86
|
+
return createEventEnvelope({
|
|
87
|
+
eventId: `${input.intent.eventId}-published`,
|
|
88
|
+
workItemKey: input.intent.workItemKey,
|
|
89
|
+
streamScope: 'work-item',
|
|
90
|
+
direction: 'outbound',
|
|
91
|
+
sourceSystem: githubPrSource,
|
|
92
|
+
sourceEventType: 'pr.comment.reply.published',
|
|
93
|
+
sourceRefs: { repo: input.repoRef, resourceUri: input.resourceUri },
|
|
94
|
+
occurredAt: input.publishedAt,
|
|
95
|
+
ingestedAt: input.publishedAt,
|
|
96
|
+
trigger: 'context-only',
|
|
97
|
+
payload: {
|
|
98
|
+
intentEventId: input.intent.eventId,
|
|
99
|
+
idempotencyKey: input.intent.payload.idempotencyKey,
|
|
100
|
+
deliveryState: 'CONFIRMED',
|
|
101
|
+
kind: input.intent.payload.kind,
|
|
102
|
+
body: input.intent.payload.body,
|
|
103
|
+
providerId: input.providerId,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function reviewCommentPublishedEvent(input) {
|
|
108
|
+
return createEventEnvelope({
|
|
109
|
+
eventId: `${input.intent.eventId}-published`,
|
|
110
|
+
workItemKey: input.intent.workItemKey,
|
|
111
|
+
streamScope: 'work-item',
|
|
112
|
+
direction: 'outbound',
|
|
113
|
+
sourceSystem: githubPrSource,
|
|
114
|
+
sourceEventType: 'pr.review-comment.reply.published',
|
|
115
|
+
sourceRefs: {
|
|
116
|
+
resourceUri: input.resourceUri,
|
|
117
|
+
sourceUrl: input.sourceUrl,
|
|
118
|
+
},
|
|
119
|
+
occurredAt: input.publishedAt,
|
|
120
|
+
ingestedAt: input.publishedAt,
|
|
121
|
+
trigger: 'context-only',
|
|
122
|
+
payload: {
|
|
123
|
+
intentEventId: input.intent.eventId,
|
|
124
|
+
idempotencyKey: input.intent.payload.idempotencyKey,
|
|
125
|
+
deliveryState: 'CONFIRMED',
|
|
126
|
+
kind: input.intent.payload.kind,
|
|
127
|
+
body: input.intent.payload.body,
|
|
128
|
+
providerId: input.providerId,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
85
132
|
async function discoverPullRequests(ingestedAt) {
|
|
86
133
|
const seenPrData = new Map();
|
|
87
134
|
const confirmedOpenRepos = new Set();
|
|
@@ -429,30 +476,30 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
429
476
|
if (ref === null) {
|
|
430
477
|
throw new Error(`cannot deliver intent ${input.event.eventId}: malformed review-thread uri ${resourceUri}`);
|
|
431
478
|
}
|
|
479
|
+
const marker = wakeIdempotencyMarker(input.event.payload.idempotencyKey);
|
|
480
|
+
if (marker !== undefined) {
|
|
481
|
+
const comments = await deps.client.listReviewComments(ref.owner, ref.repo, ref.number, deps.config.sources.github.pullRequests.commentPageSize);
|
|
482
|
+
const existing = comments.find((comment) => (comment.body ?? '').includes(marker));
|
|
483
|
+
if (existing !== undefined) {
|
|
484
|
+
return [
|
|
485
|
+
reviewCommentPublishedEvent({
|
|
486
|
+
intent: input.event,
|
|
487
|
+
resourceUri,
|
|
488
|
+
sourceUrl: existing.html_url,
|
|
489
|
+
publishedAt,
|
|
490
|
+
providerId: existing.id,
|
|
491
|
+
}),
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
432
495
|
const response = await deps.client.replyToReviewComment(ref.owner, ref.repo, ref.number, ref.rootId, formatWakeComment(input.event.payload, await readControlPlaneUiUrl(deps.config.paths.wakeRoot)));
|
|
433
496
|
return [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
sourceEventType: 'pr.review-comment.reply.published',
|
|
441
|
-
sourceRefs: {
|
|
442
|
-
resourceUri,
|
|
443
|
-
sourceUrl: response?.html_url,
|
|
444
|
-
},
|
|
445
|
-
occurredAt: publishedAt,
|
|
446
|
-
ingestedAt: publishedAt,
|
|
447
|
-
trigger: 'context-only',
|
|
448
|
-
payload: {
|
|
449
|
-
intentEventId: input.event.eventId,
|
|
450
|
-
idempotencyKey: input.event.payload.idempotencyKey,
|
|
451
|
-
deliveryState: 'CONFIRMED',
|
|
452
|
-
kind: input.event.payload.kind,
|
|
453
|
-
body: input.event.payload.body,
|
|
454
|
-
providerId: response?.id,
|
|
455
|
-
},
|
|
497
|
+
reviewCommentPublishedEvent({
|
|
498
|
+
intent: input.event,
|
|
499
|
+
resourceUri,
|
|
500
|
+
sourceUrl: response?.html_url,
|
|
501
|
+
publishedAt,
|
|
502
|
+
providerId: response?.id,
|
|
456
503
|
}),
|
|
457
504
|
];
|
|
458
505
|
}
|
|
@@ -460,27 +507,30 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
460
507
|
if (ref === null) {
|
|
461
508
|
throw new Error(`cannot deliver intent ${input.event.eventId}: malformed pr uri ${resourceUri}`);
|
|
462
509
|
}
|
|
510
|
+
const marker = wakeIdempotencyMarker(input.event.payload.idempotencyKey);
|
|
511
|
+
if (marker !== undefined) {
|
|
512
|
+
const comments = await deps.client.listComments(ref.owner, ref.repo, ref.number, deps.config.sources.github.pullRequests.commentPageSize);
|
|
513
|
+
const existing = comments.find((comment) => (comment.body ?? '').includes(marker));
|
|
514
|
+
if (existing !== undefined) {
|
|
515
|
+
return [
|
|
516
|
+
prCommentPublishedEvent({
|
|
517
|
+
intent: input.event,
|
|
518
|
+
resourceUri,
|
|
519
|
+
repoRef: ref.repoRef,
|
|
520
|
+
publishedAt,
|
|
521
|
+
providerId: existing.id,
|
|
522
|
+
}),
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
463
526
|
const response = await deps.client.createComment(ref.owner, ref.repo, ref.number, formatWakeComment(input.event.payload, await readControlPlaneUiUrl(deps.config.paths.wakeRoot)));
|
|
464
527
|
return [
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
sourceEventType: 'pr.comment.reply.published',
|
|
472
|
-
sourceRefs: { repo: ref.repoRef, resourceUri },
|
|
473
|
-
occurredAt: publishedAt,
|
|
474
|
-
ingestedAt: publishedAt,
|
|
475
|
-
trigger: 'context-only',
|
|
476
|
-
payload: {
|
|
477
|
-
intentEventId: input.event.eventId,
|
|
478
|
-
idempotencyKey: input.event.payload.idempotencyKey,
|
|
479
|
-
deliveryState: 'CONFIRMED',
|
|
480
|
-
kind: input.event.payload.kind,
|
|
481
|
-
body: input.event.payload.body,
|
|
482
|
-
providerId: response?.data?.id,
|
|
483
|
-
},
|
|
528
|
+
prCommentPublishedEvent({
|
|
529
|
+
intent: input.event,
|
|
530
|
+
resourceUri,
|
|
531
|
+
repoRef: ref.repoRef,
|
|
532
|
+
publishedAt,
|
|
533
|
+
providerId: response?.data?.id,
|
|
484
534
|
}),
|
|
485
535
|
];
|
|
486
536
|
},
|
|
@@ -502,25 +552,12 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
502
552
|
return [];
|
|
503
553
|
}
|
|
504
554
|
return [
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
sourceEventType: 'pr.review-comment.reply.published',
|
|
512
|
-
sourceRefs: { resourceUri, sourceUrl: existing.html_url },
|
|
513
|
-
occurredAt: publishedAt,
|
|
514
|
-
ingestedAt: publishedAt,
|
|
515
|
-
trigger: 'context-only',
|
|
516
|
-
payload: {
|
|
517
|
-
intentEventId: input.event.eventId,
|
|
518
|
-
idempotencyKey: input.event.payload.idempotencyKey,
|
|
519
|
-
deliveryState: 'CONFIRMED',
|
|
520
|
-
kind: input.event.payload.kind,
|
|
521
|
-
body: input.event.payload.body,
|
|
522
|
-
providerId: existing.id,
|
|
523
|
-
},
|
|
555
|
+
reviewCommentPublishedEvent({
|
|
556
|
+
intent: input.event,
|
|
557
|
+
resourceUri,
|
|
558
|
+
sourceUrl: existing.html_url,
|
|
559
|
+
publishedAt,
|
|
560
|
+
providerId: existing.id,
|
|
524
561
|
}),
|
|
525
562
|
];
|
|
526
563
|
}
|
|
@@ -534,25 +571,12 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
534
571
|
return [];
|
|
535
572
|
}
|
|
536
573
|
return [
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
sourceEventType: 'pr.comment.reply.published',
|
|
544
|
-
sourceRefs: { repo: ref.repoRef, resourceUri },
|
|
545
|
-
occurredAt: publishedAt,
|
|
546
|
-
ingestedAt: publishedAt,
|
|
547
|
-
trigger: 'context-only',
|
|
548
|
-
payload: {
|
|
549
|
-
intentEventId: input.event.eventId,
|
|
550
|
-
idempotencyKey: input.event.payload.idempotencyKey,
|
|
551
|
-
deliveryState: 'CONFIRMED',
|
|
552
|
-
kind: input.event.payload.kind,
|
|
553
|
-
body: input.event.payload.body,
|
|
554
|
-
providerId: existing.id,
|
|
555
|
-
},
|
|
574
|
+
prCommentPublishedEvent({
|
|
575
|
+
intent: input.event,
|
|
576
|
+
resourceUri,
|
|
577
|
+
repoRef: ref.repoRef,
|
|
578
|
+
publishedAt,
|
|
579
|
+
providerId: existing.id,
|
|
556
580
|
}),
|
|
557
581
|
];
|
|
558
582
|
},
|
|
@@ -59,6 +59,10 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
59
59
|
.card:hover { border-color: var(--accent); }
|
|
60
60
|
.card .title { font-weight: 600; margin-bottom: 0.25rem; }
|
|
61
61
|
.card .meta { color: #9aa2ad; font-size: 0.72rem; }
|
|
62
|
+
.child-run { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.45rem; align-items: center; margin-top: 0.45rem; padding: 0.4rem; border-radius: 6px; background: #181c22; border: 1px solid #334155; color: #cbd5e1; font-size: 0.72rem; }
|
|
63
|
+
.child-run .dot { width: 0.48rem; height: 0.48rem; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.14); }
|
|
64
|
+
.child-run .run-title { font-weight: 650; color: #e5e7eb; }
|
|
65
|
+
.child-run .run-meta { grid-column: 2; color: #94a3b8; }
|
|
62
66
|
.chip { display: inline-block; background: #2c313a; border-radius: 4px; padding: 0.05rem 0.35rem; font-size: 0.68rem; margin-right: 0.2rem; }
|
|
63
67
|
.chip-label { background: transparent; border: 1px solid #3a4150; color: #9aa2ad; margin-bottom: 0.2rem; }
|
|
64
68
|
table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
|
|
@@ -286,6 +290,14 @@ async function renderBoard(context) {
|
|
|
286
290
|
const board = await getJson('/board', context.signal);
|
|
287
291
|
if (!isActiveRequest(context.requestId)) return;
|
|
288
292
|
const main = document.getElementById('main');
|
|
293
|
+
const renderChildRun = (run) => el('div', { class: 'child-run' }, [
|
|
294
|
+
el('span', { class: 'dot' }),
|
|
295
|
+
el('div', { class: 'run-title', text: run.action + ' running' }),
|
|
296
|
+
el('div', {
|
|
297
|
+
class: 'run-meta',
|
|
298
|
+
text: [run.runnerName, run.tier, fmtMs(run.ageMs)].filter(Boolean).join(' · '),
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
289
301
|
const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
|
|
290
302
|
const items = board.filter((c) => c.condition === cond);
|
|
291
303
|
const cards = items.map((item) => el('div', {
|
|
@@ -301,6 +313,7 @@ async function renderBoard(context) {
|
|
|
301
313
|
? [el('div', { class: 'meta' }, item.labels.map((label) => el('span', { class: 'chip chip-label', text: label })))]
|
|
302
314
|
: []),
|
|
303
315
|
el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason }),
|
|
316
|
+
...((item.activeChildRuns || []).map(renderChildRun)),
|
|
304
317
|
]));
|
|
305
318
|
return el('div', { class: 'col' + (items.length === 0 ? ' col-empty' : '') }, [
|
|
306
319
|
el('h2', { text: cond + ' (' + items.length + ')' }),
|
|
@@ -74,17 +74,39 @@ function timeInStageMs(item, now) {
|
|
|
74
74
|
const lastChange = item.wake.stageHistory.at(-1)?.changedAt ?? item.wake.syncedAt;
|
|
75
75
|
return now.getTime() - Date.parse(lastChange);
|
|
76
76
|
}
|
|
77
|
+
function activeChildRunsForItem(item, runs, now) {
|
|
78
|
+
return runs
|
|
79
|
+
.filter((run) => run.workItemKey === item.workItemKey &&
|
|
80
|
+
run.status === 'running' &&
|
|
81
|
+
run.runId !== item.wake.lastRunId)
|
|
82
|
+
.sort((left, right) => left.startedAt.localeCompare(right.startedAt))
|
|
83
|
+
.map((run) => ({
|
|
84
|
+
runId: run.runId,
|
|
85
|
+
action: run.action,
|
|
86
|
+
status: run.status,
|
|
87
|
+
startedAt: run.startedAt,
|
|
88
|
+
ageMs: now.getTime() - Date.parse(run.startedAt),
|
|
89
|
+
...(run.routing?.runnerName === undefined ? {} : { runnerName: run.routing.runnerName }),
|
|
90
|
+
...(run.routing?.runnerKind === undefined ? {} : { runnerKind: run.routing.runnerKind }),
|
|
91
|
+
...(run.routing?.tier === undefined ? {} : { tier: run.routing.tier }),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
77
94
|
export async function buildBoard(input) {
|
|
78
95
|
const items = await input.stateStore.listIssueStates({
|
|
79
96
|
archiveFreshnessDays: input.config.ui.archiveFreshnessDays,
|
|
80
97
|
now: input.now,
|
|
81
98
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
// One bulk summarized scan shared by every item, instead of readRunRecord()
|
|
100
|
+
// per item - deriveCondition only reads status/sentinel, so this doesn't
|
|
101
|
+
// need full records, and doing a separate lookup (with its own fallback
|
|
102
|
+
// scan when a flat run file is missing) per item made board loads scale
|
|
103
|
+
// with items x run-history size instead of just run-history size.
|
|
104
|
+
const runs = await input.stateStore.listRunRecordSummaries();
|
|
105
|
+
const runsById = new Map(runs.map((run) => [run.runId, run]));
|
|
106
|
+
return items.map((item) => {
|
|
107
|
+
const lastRun = item.wake.lastRunId === undefined ? null : (runsById.get(item.wake.lastRunId) ?? null);
|
|
87
108
|
const { condition, reason } = deriveCondition(item, lastRun, input.config);
|
|
109
|
+
const activeChildRuns = activeChildRunsForItem(item, runs, input.now);
|
|
88
110
|
return {
|
|
89
111
|
repo: item.issue.repo,
|
|
90
112
|
number: item.issue.number,
|
|
@@ -98,6 +120,7 @@ export async function buildBoard(input) {
|
|
|
98
120
|
lastRunAction: lastRun?.action,
|
|
99
121
|
lastRunSentinel: lastRun?.sentinel,
|
|
100
122
|
lastRunStatus: lastRun?.status,
|
|
123
|
+
...(activeChildRuns.length === 0 ? {} : { activeChildRuns }),
|
|
101
124
|
sessionId: item.wake.sessionId,
|
|
102
125
|
workspacePath: item.wake.workspacePath,
|
|
103
126
|
};
|
|
@@ -109,7 +132,7 @@ export async function buildStatus(input) {
|
|
|
109
132
|
input.stateStore.readLedger(),
|
|
110
133
|
input.stateStore.isPaused(),
|
|
111
134
|
input.stateStore.listRecentEventEnvelopes({ limit: 1 }),
|
|
112
|
-
input.stateStore.
|
|
135
|
+
input.stateStore.listRunRecordSummariesForDate(today),
|
|
113
136
|
input.stateStore.listRecentRunRecords(1),
|
|
114
137
|
buildBoard(input),
|
|
115
138
|
]);
|
|
@@ -245,7 +268,7 @@ export async function buildItemDetail(input) {
|
|
|
245
268
|
if (item === null) {
|
|
246
269
|
return null;
|
|
247
270
|
}
|
|
248
|
-
const allRuns = await input.stateStore.
|
|
271
|
+
const allRuns = await input.stateStore.listRunRecordSummaries();
|
|
249
272
|
const runs = allRuns
|
|
250
273
|
.filter((run) => run.workItemKey === item.workItemKey)
|
|
251
274
|
.sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
@@ -282,7 +305,7 @@ export async function buildItemTranscripts(input) {
|
|
|
282
305
|
if (!input.config.transcripts.enabled) {
|
|
283
306
|
return { enabled: false, sessions: [] };
|
|
284
307
|
}
|
|
285
|
-
const runs = (await input.stateStore.
|
|
308
|
+
const runs = (await input.stateStore.listRunRecordSummaries()).filter((run) => run.workItemKey === input.workItemKey);
|
|
286
309
|
const runsById = new Map(runs.map((run) => [run.runId, run]));
|
|
287
310
|
const workDir = input.stateStore.paths.transcriptWorkDir(input.workItemKey);
|
|
288
311
|
const sessionDirs = (await readdir(workDir, { withFileTypes: true }).catch(() => []))
|
|
@@ -357,7 +380,7 @@ export async function buildEventsFeed(input) {
|
|
|
357
380
|
});
|
|
358
381
|
}
|
|
359
382
|
export async function buildRuns(input) {
|
|
360
|
-
const runs = await input.stateStore.
|
|
383
|
+
const runs = await input.stateStore.listRunRecordSummaries();
|
|
361
384
|
return runs
|
|
362
385
|
.filter((run) => input.status === undefined || run.status === input.status)
|
|
363
386
|
.filter((run) => input.action === undefined || run.action === input.action)
|
|
@@ -476,7 +499,7 @@ function findBucket(timestamp, buckets) {
|
|
|
476
499
|
async function listRunsForBuckets(stateStore, buckets) {
|
|
477
500
|
const dates = [...new Set(buckets.map((bucket) => bucket.bucket.slice(0, 10)))];
|
|
478
501
|
const runsById = new Map();
|
|
479
|
-
const recordsByDate = await Promise.all(dates.map((date) => stateStore.
|
|
502
|
+
const recordsByDate = await Promise.all(dates.map((date) => stateStore.listRunRecordSummariesForDate(date)));
|
|
480
503
|
for (const records of recordsByDate) {
|
|
481
504
|
for (const record of records) {
|
|
482
505
|
runsById.set(record.runId, record);
|
|
@@ -144,7 +144,14 @@ export function createStaleRunReconciler(deps) {
|
|
|
144
144
|
}
|
|
145
145
|
async function reconcileStaleRunningRecords(now) {
|
|
146
146
|
const finishedAt = now.toISOString();
|
|
147
|
-
|
|
147
|
+
// Summarized: the scan below (staleReason/classifyReconciledFailure/the
|
|
148
|
+
// newerCompletedRun comparison) only reads status/lifecycle/timestamps/pids
|
|
149
|
+
// and metadata.workspacePath, none of which are stripped - this runs every
|
|
150
|
+
// tick, so loading every run's captured stdout/raw here just to check
|
|
151
|
+
// staleness on all of them was a real OOM risk. The stale ones actually
|
|
152
|
+
// rewritten below re-fetch the full record first so captured output isn't
|
|
153
|
+
// dropped on write.
|
|
154
|
+
const runRecords = await deps.stateStore.listRunRecordSummaries();
|
|
148
155
|
await recoverMissingRunRecordClaims(runRecords, finishedAt);
|
|
149
156
|
const staleRecords = [];
|
|
150
157
|
for (const record of runRecords) {
|
|
@@ -166,15 +173,16 @@ export function createStaleRunReconciler(deps) {
|
|
|
166
173
|
// spelled out so the non-null projection is available below for its
|
|
167
174
|
// workItemKey.
|
|
168
175
|
if (projection === null || projection.wake.lastRunId !== record.runId || newerCompletedRun) {
|
|
176
|
+
const fullRecord = (await deps.stateStore.readRunRecord(record.runId)) ?? record;
|
|
169
177
|
await deps.stateStore.writeRunRecord({
|
|
170
|
-
...
|
|
178
|
+
...fullRecord,
|
|
171
179
|
lifecycle: 'TERMINAL',
|
|
172
180
|
status: 'superseded',
|
|
173
181
|
finishedAt,
|
|
174
182
|
executionOutcome: 'SUPERSEDED',
|
|
175
183
|
summary: 'Stale running record was superseded by a newer run.',
|
|
176
184
|
metadata: {
|
|
177
|
-
...
|
|
185
|
+
...fullRecord.metadata,
|
|
178
186
|
reconciledBy: 'stale-running-record',
|
|
179
187
|
supersededBy: projection?.wake.lastRunId,
|
|
180
188
|
},
|
|
@@ -183,8 +191,9 @@ export function createStaleRunReconciler(deps) {
|
|
|
183
191
|
}
|
|
184
192
|
const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' : recoveryOutcomeForLifecycle(record.lifecycle);
|
|
185
193
|
const failureContext = classifyReconciledFailure(record);
|
|
194
|
+
const fullRecord = (await deps.stateStore.readRunRecord(record.runId)) ?? record;
|
|
186
195
|
await deps.stateStore.writeRunRecord({
|
|
187
|
-
...
|
|
196
|
+
...fullRecord,
|
|
188
197
|
lifecycle: 'TERMINAL',
|
|
189
198
|
status: 'failed',
|
|
190
199
|
finishedAt,
|
|
@@ -193,7 +202,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
193
202
|
...failureContext,
|
|
194
203
|
summary: `Run exceeded timeout while marked running and was reconciled by a later tick.`,
|
|
195
204
|
metadata: {
|
|
196
|
-
...
|
|
205
|
+
...fullRecord.metadata,
|
|
197
206
|
reconciledBy: 'stale-running-record',
|
|
198
207
|
recoveryLifecycle: record.lifecycle,
|
|
199
208
|
staleReason: reason,
|
|
@@ -26,11 +26,28 @@ import { currentProcessIdentity, processIdentityMatches } from '../lib/process-i
|
|
|
26
26
|
import { readJsonFile, writeJsonFile } from '../lib/json-file.js';
|
|
27
27
|
import { isMissingPathError } from '../lib/state-health.js';
|
|
28
28
|
const prReviewApprovalMarker = '<!-- wake:pr-review-approved -->';
|
|
29
|
+
const prReviewChangesMarker = '<!-- wake:pr-review-changes-requested -->';
|
|
29
30
|
const activeRunSourceRefreshIntervalMs = 1_000;
|
|
30
31
|
function latestHumanCommentId(candidate) {
|
|
31
32
|
const human = candidate.comments.filter((c) => !c.isBotAuthored);
|
|
32
33
|
return human.at(-1)?.id;
|
|
33
34
|
}
|
|
35
|
+
function latestActionableCommentId(candidate) {
|
|
36
|
+
const handledCommentId = typeof candidate.context.lastHandledCommentId === 'string'
|
|
37
|
+
? candidate.context.lastHandledCommentId
|
|
38
|
+
: undefined;
|
|
39
|
+
const lastBotIndex = candidate.comments.reduce((acc, comment, index) => {
|
|
40
|
+
return comment.isBotAuthored ? index : acc;
|
|
41
|
+
}, -1);
|
|
42
|
+
const latestComment = candidate.comments.slice(lastBotIndex).at(-1);
|
|
43
|
+
if (latestComment?.id !== handledCommentId &&
|
|
44
|
+
latestComment?.isBotAuthored === true &&
|
|
45
|
+
latestComment.resourceUri !== undefined &&
|
|
46
|
+
latestComment.body.includes(prReviewChangesMarker)) {
|
|
47
|
+
return latestComment.id;
|
|
48
|
+
}
|
|
49
|
+
return latestHumanCommentId(candidate);
|
|
50
|
+
}
|
|
34
51
|
function projectedSourceRevision(projection) {
|
|
35
52
|
const latestCommentUpdatedAt = projection.comments
|
|
36
53
|
.map((comment) => comment.updatedAt)
|
|
@@ -43,6 +60,12 @@ function projectedSourceRevision(projection) {
|
|
|
43
60
|
function isLateralReadOnlyAction(action, config) {
|
|
44
61
|
return isCustomCommandAction(action, config);
|
|
45
62
|
}
|
|
63
|
+
function isEventFromWatcherRun(event) {
|
|
64
|
+
return (typeof event.payload === 'object' &&
|
|
65
|
+
event.payload !== null &&
|
|
66
|
+
'watcherRun' in event.payload &&
|
|
67
|
+
event.payload.watcherRun === true);
|
|
68
|
+
}
|
|
46
69
|
function shouldPublishRunResult(input) {
|
|
47
70
|
if (input.failureClass === 'quota') {
|
|
48
71
|
return false;
|
|
@@ -462,7 +485,7 @@ export function createTickRunner(deps) {
|
|
|
462
485
|
async function markPendingActionableIssues(projections) {
|
|
463
486
|
const activeRunWorkItemKeys = new Set();
|
|
464
487
|
const now = deps.clock.now();
|
|
465
|
-
for (const record of await deps.stateStore.
|
|
488
|
+
for (const record of await deps.stateStore.listRunRecordSummaries()) {
|
|
466
489
|
if (record.status === 'running' && (await isRunningRecordActive(record, now))) {
|
|
467
490
|
activeRunWorkItemKeys.add(record.workItemKey);
|
|
468
491
|
}
|
|
@@ -501,7 +524,7 @@ export function createTickRunner(deps) {
|
|
|
501
524
|
async function exceedsDispatchRateLimit(now) {
|
|
502
525
|
const { windowMs, maxDispatches } = deps.config.scheduler.dispatchRateLimit;
|
|
503
526
|
const windowStartMs = now.getTime() - windowMs;
|
|
504
|
-
const runRecords = await deps.stateStore.
|
|
527
|
+
const runRecords = await deps.stateStore.listRunRecordSummaries();
|
|
505
528
|
const recentCount = runRecords.reduce((count, record) => {
|
|
506
529
|
const startedAtMs = Date.parse(record.startedAt);
|
|
507
530
|
return Number.isFinite(startedAtMs) &&
|
|
@@ -541,7 +564,7 @@ export function createTickRunner(deps) {
|
|
|
541
564
|
}));
|
|
542
565
|
}
|
|
543
566
|
async function hasSchedulerCapacity(now) {
|
|
544
|
-
const runRecords = await deps.stateStore.
|
|
567
|
+
const runRecords = await deps.stateStore.listRunRecordSummaries();
|
|
545
568
|
for (const record of runRecords) {
|
|
546
569
|
if (record.status !== 'running') {
|
|
547
570
|
continue;
|
|
@@ -763,6 +786,7 @@ export function createTickRunner(deps) {
|
|
|
763
786
|
});
|
|
764
787
|
const matchingEvents = events
|
|
765
788
|
.filter((event) => watcher.on.event.includes(event.sourceEventType))
|
|
789
|
+
.filter((event) => !isEventFromWatcherRun(event))
|
|
766
790
|
.sort((left, right) => left.ingestedAt.localeCompare(right.ingestedAt));
|
|
767
791
|
const cursorIndex = state?.lastDispatchedEventId === undefined
|
|
768
792
|
? -1
|
|
@@ -881,12 +905,12 @@ export function createTickRunner(deps) {
|
|
|
881
905
|
if (await parkConfigDriftedProjections(projections)) {
|
|
882
906
|
return { status: 'processed' };
|
|
883
907
|
}
|
|
884
|
-
|
|
885
|
-
|
|
908
|
+
let candidate = projections.find((issue) => policy.resolveNextEligibleAction(issue, deps.config) !== null);
|
|
909
|
+
const watcherDispatch = candidate === undefined ? await nextWatcherDispatch(projections, tickStartedAt) : null;
|
|
910
|
+
candidate ??= watcherDispatch?.projection;
|
|
886
911
|
let watcherStateKeyForRun;
|
|
887
912
|
let watcherTriggerForRun;
|
|
888
913
|
const watcherRun = watcherDispatch !== null;
|
|
889
|
-
candidate ??= projections.find((issue) => policy.resolveNextEligibleAction(issue, deps.config) !== null);
|
|
890
914
|
if (candidate === undefined) {
|
|
891
915
|
return { status: 'idle' };
|
|
892
916
|
}
|
|
@@ -1669,7 +1693,7 @@ export function createTickRunner(deps) {
|
|
|
1669
1693
|
// unset lets the next tick retry instead of silently eating the request (S9).
|
|
1670
1694
|
...(runnerResult.failureClass === 'quota' || runnerResult.failureClass === 'infra'
|
|
1671
1695
|
? {}
|
|
1672
|
-
: { handledCommentId:
|
|
1696
|
+
: { handledCommentId: latestActionableCommentId(candidate) }),
|
|
1673
1697
|
body: parsedRunnerResult.body,
|
|
1674
1698
|
envelope: parsedRunnerResult.envelope,
|
|
1675
1699
|
executionOutcome,
|
package/dist/src/lib/paths.js
CHANGED
|
@@ -32,6 +32,8 @@ export function createWakePaths(wakeRoot) {
|
|
|
32
32
|
sourceStateFile: (source, key) => join(dataRoot, 'sources', sanitizePathKey(source), `${sanitizePathKey(key)}.json`),
|
|
33
33
|
runFile: (runId) => join(dataRoot, 'runs', `${runId}.json`),
|
|
34
34
|
runDateFile: (date, runId) => join(dataRoot, 'runs', 'by-date', date, `${runId}.json`),
|
|
35
|
+
runDateIndexFile: (date) => join(dataRoot, 'runs', 'by-date', date, 'index.json'),
|
|
36
|
+
runDateIndexLockFile: (date) => join(dataRoot, 'locks', `run-index-${date}.lock`),
|
|
35
37
|
eventFile: (date) => join(dataRoot, 'events', `${date}.jsonl`),
|
|
36
38
|
eventEnvelopeFile: (eventId) => join(dataRoot, 'events-by-id', `${sanitizePathKey(eventId)}.json`),
|
|
37
39
|
logFile: (date) => join(dataRoot, 'logs', `${date}.log`),
|
package/dist/src/version.js
CHANGED