@holon-run/agentinbox 1.8.1 → 1.9.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/dist/src/adapters.js +6 -0
- package/dist/src/service.js +56 -3
- package/dist/src/sources/github_ci.js +211 -1
- package/dist/src/sources/remote.js +10 -0
- package/dist/src/sources/remote_modules.js +9 -16
- package/dist/src/store.js +8 -0
- package/drizzle/migrations/0009_inbox_aggregation_default_on.sql +8 -0
- package/package.json +1 -1
package/dist/src/adapters.js
CHANGED
|
@@ -213,6 +213,12 @@ class AdapterRegistry {
|
|
|
213
213
|
}
|
|
214
214
|
return this.remoteSource.summarizeDigestThread(source, items, grouping);
|
|
215
215
|
}
|
|
216
|
+
async shouldFlushDigestThread(source, items, context) {
|
|
217
|
+
if (source.sourceType === "local_event") {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return this.remoteSource.shouldFlushDigestThread(source, items, context);
|
|
221
|
+
}
|
|
216
222
|
async listDeliveryOperations(source, handle) {
|
|
217
223
|
const module = await this.resolveDeliveryModule(source, handle);
|
|
218
224
|
if (!module?.listDeliveryOperations) {
|
package/dist/src/service.js
CHANGED
|
@@ -30,6 +30,7 @@ const DEFAULT_IDLE_SOURCE_GRACE_MS = 30 * 60 * 1000;
|
|
|
30
30
|
const DEFAULT_INBOX_AGGREGATION_WINDOW_MS = 30 * 1000;
|
|
31
31
|
const DEFAULT_INBOX_AGGREGATION_MAX_ITEMS = 20;
|
|
32
32
|
const DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS = 24 * 60 * 60 * 1000;
|
|
33
|
+
const DEFAULT_DIGEST_THREAD_RECHECK_MS = 30_000;
|
|
33
34
|
const DIRECT_INBOX_SOURCE_ID = "__agentinbox_direct_text__";
|
|
34
35
|
const DIRECT_INBOX_EVENT_VARIANT = "agentinbox.direct_text_message";
|
|
35
36
|
const TIMER_INBOX_SOURCE_ID = "__agentinbox_timer__";
|
|
@@ -1339,7 +1340,10 @@ class AgentInboxService {
|
|
|
1339
1340
|
this.store.closeDigestThread(thread.threadId, now);
|
|
1340
1341
|
thread = null;
|
|
1341
1342
|
}
|
|
1342
|
-
const
|
|
1343
|
+
const groupingFlushDelayMs = grouping.flushDelayMs != null && grouping.flushDelayMs >= 0
|
|
1344
|
+
? grouping.flushDelayMs
|
|
1345
|
+
: null;
|
|
1346
|
+
const flushAfterAt = new Date(Date.now() + (groupingFlushDelayMs ?? policy.windowMs ?? DEFAULT_INBOX_AGGREGATION_WINDOW_MS)).toISOString();
|
|
1343
1347
|
if (!thread) {
|
|
1344
1348
|
thread = this.store.createDigestThread({
|
|
1345
1349
|
inboxId: input.inbox.inboxId,
|
|
@@ -1621,7 +1625,7 @@ class AgentInboxService {
|
|
|
1621
1625
|
source,
|
|
1622
1626
|
subscriptionId: subscription.subscriptionId,
|
|
1623
1627
|
item,
|
|
1624
|
-
summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant),
|
|
1628
|
+
summary: summarizeSourceEvent(source.sourceType, source.sourceKey, event.eventVariant, event.metadata),
|
|
1625
1629
|
});
|
|
1626
1630
|
insertedEntries.push(...entries);
|
|
1627
1631
|
for (const entry of entries) {
|
|
@@ -1953,6 +1957,7 @@ class AgentInboxService {
|
|
|
1953
1957
|
const inbox = {
|
|
1954
1958
|
inboxId: (0, util_1.generateCanonicalId)("inb"),
|
|
1955
1959
|
ownerAgentId: agentId,
|
|
1960
|
+
aggregationEnabled: true,
|
|
1956
1961
|
createdAt: (0, util_1.nowIso)(),
|
|
1957
1962
|
};
|
|
1958
1963
|
this.store.insertInbox(inbox);
|
|
@@ -3199,6 +3204,21 @@ class AgentInboxService {
|
|
|
3199
3204
|
this.store.closeDigestThread(thread.threadId, now);
|
|
3200
3205
|
continue;
|
|
3201
3206
|
}
|
|
3207
|
+
const pendingItems = this.store.listUnackedInboxItemsForDigestThread(thread.threadId);
|
|
3208
|
+
if (pendingItems.length > 0) {
|
|
3209
|
+
const decision = await this.adapters.shouldFlushDigestThread(source, pendingItems.map((item) => activationItemFromInboxItem(item)), {
|
|
3210
|
+
threadCreatedAt: thread.createdAt,
|
|
3211
|
+
lastItemAt: thread.lastItemAt,
|
|
3212
|
+
now,
|
|
3213
|
+
});
|
|
3214
|
+
if (decision && !decision.flush) {
|
|
3215
|
+
const recheckAfterMs = decision.recheckAfterMs != null && decision.recheckAfterMs > 0
|
|
3216
|
+
? decision.recheckAfterMs
|
|
3217
|
+
: DEFAULT_DIGEST_THREAD_RECHECK_MS;
|
|
3218
|
+
this.store.updateDigestThreadFlushAfterAt(thread.threadId, new Date(Date.parse(now) + recheckAfterMs).toISOString());
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3202
3222
|
const entry = await this.flushDigestThread(source, thread.threadId);
|
|
3203
3223
|
if (!entry) {
|
|
3204
3224
|
continue;
|
|
@@ -3945,7 +3965,27 @@ function extractHttpStatus(error) {
|
|
|
3945
3965
|
}
|
|
3946
3966
|
return null;
|
|
3947
3967
|
}
|
|
3948
|
-
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
3968
|
+
function summarizeSourceEvent(sourceType, sourceKey, eventVariant, metadata = {}) {
|
|
3969
|
+
if (sourceType === "github_repo") {
|
|
3970
|
+
const repo = asNonEmptyString(metadata.repoFullName) ?? sourceKey;
|
|
3971
|
+
const number = typeof metadata.number === "number" && Number.isInteger(metadata.number)
|
|
3972
|
+
? metadata.number
|
|
3973
|
+
: null;
|
|
3974
|
+
const isPullRequest = metadata.isPullRequest === true;
|
|
3975
|
+
const resource = number == null ? null : `${isPullRequest ? "PR" : "Issue"} #${number}`;
|
|
3976
|
+
const action = asNonEmptyString(metadata.action) ?? eventVariant.split(".")[1] ?? "updated";
|
|
3977
|
+
const title = asNonEmptyString(metadata.title);
|
|
3978
|
+
const author = asNonEmptyString(metadata.author);
|
|
3979
|
+
const url = asNonEmptyString(metadata.url);
|
|
3980
|
+
const details = [
|
|
3981
|
+
resource,
|
|
3982
|
+
action,
|
|
3983
|
+
title ? `"${truncateSummary(title, 160)}"` : null,
|
|
3984
|
+
author ? `by @${author}` : null,
|
|
3985
|
+
url,
|
|
3986
|
+
].filter((value) => value != null);
|
|
3987
|
+
return `${sourceType}:${repo}${details.length > 0 ? ` ${details.join(" ")}` : `:${eventVariant}`}`;
|
|
3988
|
+
}
|
|
3949
3989
|
if (sourceType === "github_repo_ci") {
|
|
3950
3990
|
const parts = eventVariant.split(".");
|
|
3951
3991
|
const [, second, third, fourth] = parts;
|
|
@@ -3967,6 +4007,19 @@ function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
|
3967
4007
|
}
|
|
3968
4008
|
return `${sourceType}:${sourceKey}:${eventVariant}`;
|
|
3969
4009
|
}
|
|
4010
|
+
function asNonEmptyString(value) {
|
|
4011
|
+
if (typeof value !== "string") {
|
|
4012
|
+
return null;
|
|
4013
|
+
}
|
|
4014
|
+
const trimmed = value.trim();
|
|
4015
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
4016
|
+
}
|
|
4017
|
+
function truncateSummary(value, maxChars) {
|
|
4018
|
+
if (value.length <= maxChars) {
|
|
4019
|
+
return value;
|
|
4020
|
+
}
|
|
4021
|
+
return `${value.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
4022
|
+
}
|
|
3970
4023
|
function summarizeDirectInboxMessage(sender) {
|
|
3971
4024
|
if (sender) {
|
|
3972
4025
|
return `direct_text_message:${sender}`;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GithubCiSourceRuntime = exports.GithubActionsUxcClient = exports.DEFAULT_GITHUB_CI_PER_PAGE = exports.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS = exports.GITHUB_CI_ENDPOINT = void 0;
|
|
3
|
+
exports.GithubCiSourceRuntime = exports.GithubActionsUxcClient = exports.DEFAULT_GITHUB_CI_DIGEST_FLUSH_TIMEOUT_SECS = exports.DEFAULT_GITHUB_CI_DIGEST_QUIET_WINDOW_SECS = exports.DEFAULT_GITHUB_CI_PER_PAGE = exports.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS = exports.GITHUB_CI_ENDPOINT = void 0;
|
|
4
4
|
exports.normalizeGithubWorkflowRunEvent = normalizeGithubWorkflowRunEvent;
|
|
5
5
|
exports.parseGithubCiSourceConfig = parseGithubCiSourceConfig;
|
|
6
|
+
exports.githubCiDigestQuietWindowMs = githubCiDigestQuietWindowMs;
|
|
7
|
+
exports.githubCiDigestFlushTimeoutMs = githubCiDigestFlushTimeoutMs;
|
|
8
|
+
exports.deriveGithubCiNotificationGrouping = deriveGithubCiNotificationGrouping;
|
|
9
|
+
exports.decideGithubCiDigestFlush = decideGithubCiDigestFlush;
|
|
10
|
+
exports.summarizeGithubCiDigestThread = summarizeGithubCiDigestThread;
|
|
6
11
|
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
7
12
|
exports.GITHUB_CI_ENDPOINT = "https://api.github.com";
|
|
8
13
|
exports.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS = 30;
|
|
9
14
|
exports.DEFAULT_GITHUB_CI_PER_PAGE = 20;
|
|
15
|
+
exports.DEFAULT_GITHUB_CI_DIGEST_QUIET_WINDOW_SECS = 120;
|
|
16
|
+
exports.DEFAULT_GITHUB_CI_DIGEST_FLUSH_TIMEOUT_SECS = 60 * 60;
|
|
17
|
+
const GITHUB_CI_DIGEST_RECHECK_MS = 30_000;
|
|
18
|
+
const GITHUB_CI_DIGEST_MIN_RECHECK_MS = 1_000;
|
|
10
19
|
const MAX_PAGES_PER_SYNC = 10;
|
|
11
20
|
const MAX_SEEN_KEYS = 512;
|
|
12
21
|
const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
|
|
@@ -335,6 +344,8 @@ function parseGithubCiSourceConfig(source) {
|
|
|
335
344
|
eventFilter: asString(config.eventFilter) ?? undefined,
|
|
336
345
|
branch: asString(config.branch) ?? undefined,
|
|
337
346
|
statusFilter: asString(config.statusFilter) ?? undefined,
|
|
347
|
+
digestQuietWindowSecs: asNumber(config.digestQuietWindowSecs) ?? undefined,
|
|
348
|
+
digestFlushTimeoutSecs: asNumber(config.digestFlushTimeoutSecs) ?? undefined,
|
|
338
349
|
};
|
|
339
350
|
}
|
|
340
351
|
return {
|
|
@@ -346,8 +357,207 @@ function parseGithubCiSourceConfig(source) {
|
|
|
346
357
|
eventFilter: asString(config.eventFilter) ?? undefined,
|
|
347
358
|
branch: asString(config.branch) ?? undefined,
|
|
348
359
|
statusFilter: asString(config.statusFilter) ?? undefined,
|
|
360
|
+
digestQuietWindowSecs: asNumber(config.digestQuietWindowSecs) ?? undefined,
|
|
361
|
+
digestFlushTimeoutSecs: asNumber(config.digestFlushTimeoutSecs) ?? undefined,
|
|
349
362
|
};
|
|
350
363
|
}
|
|
364
|
+
function githubCiDigestQuietWindowMs(config) {
|
|
365
|
+
return Math.max(0, (config.digestQuietWindowSecs ?? exports.DEFAULT_GITHUB_CI_DIGEST_QUIET_WINDOW_SECS) * 1000);
|
|
366
|
+
}
|
|
367
|
+
function githubCiDigestFlushTimeoutMs(config) {
|
|
368
|
+
return Math.max(0, (config.digestFlushTimeoutSecs ?? exports.DEFAULT_GITHUB_CI_DIGEST_FLUSH_TIMEOUT_SECS) * 1000);
|
|
369
|
+
}
|
|
370
|
+
function firstPullRequestNumber(item) {
|
|
371
|
+
const numbers = item.metadata.pullRequestNumbers;
|
|
372
|
+
if (!Array.isArray(numbers)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
for (const number of numbers) {
|
|
376
|
+
if (typeof number === "number" && Number.isInteger(number) && number > 0) {
|
|
377
|
+
return number;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Groups workflow-run notifications at the pull-request level so a push that
|
|
384
|
+
* fans out into many workflow runs produces one digest thread per PR instead
|
|
385
|
+
* of one per run. Runs without a PR association fall back to per-push grouping
|
|
386
|
+
* by head sha.
|
|
387
|
+
*/
|
|
388
|
+
function deriveGithubCiNotificationGrouping(item, config) {
|
|
389
|
+
const workflowRunId = asNumber(item.metadata.workflowRunId);
|
|
390
|
+
if (!workflowRunId) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const repoFullName = asString(item.metadata.repoFullName) ?? `${config.owner}/${config.repo}`;
|
|
394
|
+
const headSha = asString(item.metadata.headSha);
|
|
395
|
+
const prNumber = firstPullRequestNumber(item);
|
|
396
|
+
const resourceRef = prNumber != null
|
|
397
|
+
? `pr:${repoFullName}#${prNumber}`
|
|
398
|
+
: headSha
|
|
399
|
+
? `push:${repoFullName}:${headSha}`
|
|
400
|
+
: null;
|
|
401
|
+
if (!resourceRef) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const conclusion = asString(item.metadata.conclusion);
|
|
405
|
+
const actionableFailure = Boolean(conclusion) && conclusion !== "success" && conclusion !== "skipped";
|
|
406
|
+
return {
|
|
407
|
+
groupable: true,
|
|
408
|
+
resourceRef,
|
|
409
|
+
eventFamily: "ci_updates",
|
|
410
|
+
summaryHint: prNumber != null
|
|
411
|
+
? `CI updates for ${repoFullName}#${prNumber}`
|
|
412
|
+
: `CI updates for ${repoFullName}@${shortSha(headSha)}`,
|
|
413
|
+
flushClass: actionableFailure ? "immediate" : "normal",
|
|
414
|
+
flushDelayMs: githubCiDigestQuietWindowMs(config),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Post-hoc flush decision for a PR-level CI digest thread: flush once every
|
|
419
|
+
* workflow run observed for the thread's latest head sha has reached a
|
|
420
|
+
* terminal state (or the hard timeout expires). GitHub exposes no prior about
|
|
421
|
+
* how many runs a push will create, so readiness is judged from the observed
|
|
422
|
+
* event stream itself.
|
|
423
|
+
*/
|
|
424
|
+
function decideGithubCiDigestFlush(items, config, context) {
|
|
425
|
+
if (items.length === 0) {
|
|
426
|
+
return { flush: true, reason: "no pending items" };
|
|
427
|
+
}
|
|
428
|
+
const ordered = orderItemsByOccurredAt(items);
|
|
429
|
+
const latestSha = latestHeadSha(ordered);
|
|
430
|
+
if (!latestSha) {
|
|
431
|
+
return { flush: true, reason: "no head sha" };
|
|
432
|
+
}
|
|
433
|
+
const nowMs = Date.parse(context.now);
|
|
434
|
+
const createdMs = Date.parse(context.threadCreatedAt);
|
|
435
|
+
const timeoutMs = githubCiDigestFlushTimeoutMs(config);
|
|
436
|
+
const timedOut = Number.isFinite(nowMs) && Number.isFinite(createdMs) && nowMs - createdMs >= timeoutMs;
|
|
437
|
+
if (!timedOut) {
|
|
438
|
+
const runTerminal = new Map();
|
|
439
|
+
for (const item of ordered) {
|
|
440
|
+
if (asString(item.metadata.headSha) !== latestSha) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const runId = asNumber(item.metadata.workflowRunId);
|
|
444
|
+
if (!runId) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const status = asString(item.metadata.status);
|
|
448
|
+
const conclusion = asString(item.metadata.conclusion);
|
|
449
|
+
// Sticky terminal: out-of-order delivery or timestamp skew must never
|
|
450
|
+
// downgrade a run that was already observed terminal back to pending,
|
|
451
|
+
// which would defer the flush until the hard timeout.
|
|
452
|
+
const observedTerminal = status === "completed" || Boolean(conclusion);
|
|
453
|
+
runTerminal.set(runId, (runTerminal.get(runId) ?? false) || observedTerminal);
|
|
454
|
+
}
|
|
455
|
+
let pending = 0;
|
|
456
|
+
for (const terminal of runTerminal.values()) {
|
|
457
|
+
if (!terminal) {
|
|
458
|
+
pending += 1;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (pending > 0) {
|
|
462
|
+
const remaining = createdMs + timeoutMs - nowMs;
|
|
463
|
+
const recheckAfterMs = Number.isFinite(remaining)
|
|
464
|
+
? Math.min(GITHUB_CI_DIGEST_RECHECK_MS, Math.max(GITHUB_CI_DIGEST_MIN_RECHECK_MS, remaining))
|
|
465
|
+
: GITHUB_CI_DIGEST_RECHECK_MS;
|
|
466
|
+
return {
|
|
467
|
+
flush: false,
|
|
468
|
+
reason: `${pending} run(s) still pending on ${shortSha(latestSha)}`,
|
|
469
|
+
recheckAfterMs,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
flush: true,
|
|
475
|
+
reason: timedOut ? `hard timeout on ${shortSha(latestSha)}` : `all runs terminal on ${shortSha(latestSha)}`,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function summarizeGithubCiDigestThread(items) {
|
|
479
|
+
if (items.length === 0) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const ordered = orderItemsByOccurredAt(items);
|
|
483
|
+
let repoFullName = null;
|
|
484
|
+
let prNumber = null;
|
|
485
|
+
let latestSha = null;
|
|
486
|
+
for (const item of ordered) {
|
|
487
|
+
repoFullName = asString(item.metadata.repoFullName) ?? repoFullName;
|
|
488
|
+
prNumber = firstPullRequestNumber(item) ?? prNumber;
|
|
489
|
+
latestSha = asString(item.metadata.headSha) ?? latestSha;
|
|
490
|
+
}
|
|
491
|
+
const runs = new Map();
|
|
492
|
+
for (const item of ordered) {
|
|
493
|
+
const runId = asNumber(item.metadata.workflowRunId);
|
|
494
|
+
if (!runId) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const previous = runs.get(runId) ?? { name: null, conclusion: null };
|
|
498
|
+
runs.set(runId, {
|
|
499
|
+
name: asString(item.metadata.name) ?? previous.name,
|
|
500
|
+
// Sticky conclusion: keep the first observed terminal conclusion so
|
|
501
|
+
// out-of-order items cannot rewrite a run's reported outcome.
|
|
502
|
+
conclusion: previous.conclusion ?? asString(item.metadata.conclusion),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const failed = [];
|
|
506
|
+
let passed = 0;
|
|
507
|
+
let running = 0;
|
|
508
|
+
for (const run of runs.values()) {
|
|
509
|
+
if (run.conclusion === "success" || run.conclusion === "skipped") {
|
|
510
|
+
passed += 1;
|
|
511
|
+
}
|
|
512
|
+
else if (run.conclusion) {
|
|
513
|
+
failed.push(run.name ? `${run.name} (${run.conclusion})` : run.conclusion);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
running += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const target = repoFullName
|
|
520
|
+
? prNumber != null
|
|
521
|
+
? `${repoFullName}#${prNumber}`
|
|
522
|
+
: latestSha
|
|
523
|
+
? `${repoFullName}@${shortSha(latestSha)}`
|
|
524
|
+
: repoFullName
|
|
525
|
+
: "repository";
|
|
526
|
+
const parts = [`${runs.size} workflow runs on ${shortSha(latestSha)}`];
|
|
527
|
+
if (failed.length > 0) {
|
|
528
|
+
const shown = failed.slice(0, 3).join(", ");
|
|
529
|
+
parts.push(`${failed.length} failed: ${shown}${failed.length > 3 ? ", …" : ""}`);
|
|
530
|
+
}
|
|
531
|
+
if (passed > 0) {
|
|
532
|
+
parts.push(`${passed} passed`);
|
|
533
|
+
}
|
|
534
|
+
if (running > 0) {
|
|
535
|
+
parts.push(`${running} still running`);
|
|
536
|
+
}
|
|
537
|
+
return `CI for ${target}: ${parts.join(", ")}`;
|
|
538
|
+
}
|
|
539
|
+
function orderItemsByOccurredAt(items) {
|
|
540
|
+
return [...items].sort((left, right) => Date.parse(left.occurredAt) - Date.parse(right.occurredAt));
|
|
541
|
+
}
|
|
542
|
+
function latestHeadSha(orderedItems) {
|
|
543
|
+
let latestSha = null;
|
|
544
|
+
let latestAt = Number.NaN;
|
|
545
|
+
for (const item of orderedItems) {
|
|
546
|
+
const sha = asString(item.metadata.headSha);
|
|
547
|
+
if (!sha) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const at = Date.parse(item.occurredAt);
|
|
551
|
+
if (latestSha == null || (!Number.isNaN(at) && (Number.isNaN(latestAt) || at >= latestAt))) {
|
|
552
|
+
latestSha = sha;
|
|
553
|
+
latestAt = at;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return latestSha;
|
|
557
|
+
}
|
|
558
|
+
function shortSha(sha) {
|
|
559
|
+
return sha ? sha.slice(0, 7) : "unknown";
|
|
560
|
+
}
|
|
351
561
|
function parseGithubCiCheckpoint(checkpoint) {
|
|
352
562
|
if (!checkpoint) {
|
|
353
563
|
return {};
|
|
@@ -299,6 +299,16 @@ class RemoteSourceRuntime {
|
|
|
299
299
|
}
|
|
300
300
|
return module.deriveNotificationGrouping(item, moduleInputSource(source));
|
|
301
301
|
}
|
|
302
|
+
async shouldFlushDigestThread(source, items, context) {
|
|
303
|
+
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const module = await this.moduleRegistry.resolve(source, this.homeDir);
|
|
307
|
+
if (typeof module.shouldFlushDigestThread !== "function") {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return module.shouldFlushDigestThread(items, moduleInputSource(source), context);
|
|
311
|
+
}
|
|
302
312
|
async summarizeDigestThread(source, items, grouping) {
|
|
303
313
|
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
304
314
|
return null;
|
|
@@ -123,6 +123,7 @@ function validateModuleContract(module, sourcePath) {
|
|
|
123
123
|
validateOptionalHook(module.projectLifecycleSignal, "projectLifecycleSignal", sourcePath);
|
|
124
124
|
validateOptionalHook(module.deriveInlinePreview, "deriveInlinePreview", sourcePath);
|
|
125
125
|
validateOptionalHook(module.deriveNotificationGrouping, "deriveNotificationGrouping", sourcePath);
|
|
126
|
+
validateOptionalHook(module.shouldFlushDigestThread, "shouldFlushDigestThread", sourcePath);
|
|
126
127
|
validateOptionalHook(module.listDeliveryOperations, "listDeliveryOperations", sourcePath);
|
|
127
128
|
validateOptionalHook(module.invokeDeliveryOperation, "invokeDeliveryOperation", sourcePath);
|
|
128
129
|
validateOptionalHook(module.listSourceOperations, "listSourceOperations", sourcePath);
|
|
@@ -401,26 +402,18 @@ const GITHUB_REPO_CI_MODULE = {
|
|
|
401
402
|
}
|
|
402
403
|
return parts.join(" ");
|
|
403
404
|
},
|
|
404
|
-
deriveNotificationGrouping(item) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const conclusion = asNonEmptyString(item.metadata.conclusion);
|
|
410
|
-
const status = asNonEmptyString(item.metadata.status);
|
|
411
|
-
return {
|
|
412
|
-
groupable: true,
|
|
413
|
-
resourceRef: `workflow_run:${workflowRunId}`,
|
|
414
|
-
eventFamily: "ci_updates",
|
|
415
|
-
summaryHint: `CI updates for run #${workflowRunId}`,
|
|
416
|
-
flushClass: conclusion || status === "completed" ? "immediate" : "normal",
|
|
417
|
-
};
|
|
405
|
+
deriveNotificationGrouping(item, source) {
|
|
406
|
+
return (0, github_ci_1.deriveGithubCiNotificationGrouping)(item, (0, github_ci_1.parseGithubCiSourceConfig)(source));
|
|
407
|
+
},
|
|
408
|
+
shouldFlushDigestThread(items, source, context) {
|
|
409
|
+
return (0, github_ci_1.decideGithubCiDigestFlush)(items, (0, github_ci_1.parseGithubCiSourceConfig)(source), context);
|
|
418
410
|
},
|
|
419
411
|
summarizeDigestThread(items, _source, grouping) {
|
|
420
|
-
if (
|
|
412
|
+
if (items.length === 0) {
|
|
421
413
|
return null;
|
|
422
414
|
}
|
|
423
|
-
return
|
|
415
|
+
return (0, github_ci_1.summarizeGithubCiDigestThread)(items)
|
|
416
|
+
?? (grouping.summaryHint ? `${items.length} ${grouping.summaryHint}` : null);
|
|
424
417
|
},
|
|
425
418
|
validateConfig(source) {
|
|
426
419
|
(0, github_ci_1.parseGithubCiSourceConfig)(source);
|
package/dist/src/store.js
CHANGED
|
@@ -1710,6 +1710,14 @@ class AgentInboxStore {
|
|
|
1710
1710
|
const rows = this.getAll("select * from digest_threads where status = 'open' order by created_at asc, thread_id asc");
|
|
1711
1711
|
return rows.map((row) => this.mapDigestThread(row));
|
|
1712
1712
|
}
|
|
1713
|
+
updateDigestThreadFlushAfterAt(threadId, flushAfterAt) {
|
|
1714
|
+
this.run(`
|
|
1715
|
+
update digest_threads
|
|
1716
|
+
set flush_after_at = ?, updated_at = ?
|
|
1717
|
+
where thread_id = ? and status = 'open'
|
|
1718
|
+
`, [flushAfterAt, (0, util_1.nowIso)(), threadId]);
|
|
1719
|
+
this.persist();
|
|
1720
|
+
}
|
|
1713
1721
|
materializeDigestSnapshot(input) {
|
|
1714
1722
|
const thread = this.getDigestThread(input.threadId);
|
|
1715
1723
|
if (!thread) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Aggregation is now enabled by default for agent inboxes: bursty events on
|
|
2
|
+
-- the same resource collapse into one digest notification instead of waking
|
|
3
|
+
-- the agent once per event. Existing inboxes were left disabled by the old
|
|
4
|
+
-- default, so flip them on in bulk; explicit opt-outs can set the flag back
|
|
5
|
+
-- to 0 per inbox via the inbox aggregation policy.
|
|
6
|
+
update inboxes
|
|
7
|
+
set aggregation_enabled = 1
|
|
8
|
+
where aggregation_enabled = 0;
|