@songsid/agend 2.3.2 → 2.3.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-whats-new.d.ts +21 -0
- package/dist/agent-whats-new.js +52 -23
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/authority/authority-state.js +1 -1
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/web-authority.d.ts +9 -4
- package/dist/authority/web-authority.js +10 -5
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/claude-code.d.ts +13 -1
- package/dist/backend/claude-code.js +20 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +16 -3
- package/dist/backend/codex-session-lookup.js +45 -11
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/kiro-reconcile-store.js +8 -1
- package/dist/backend/kiro-reconcile-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +0 -5
- package/dist/backend/kiro.js +0 -10
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +7 -4
- package/dist/backend/types.js.map +1 -1
- package/dist/cli.js +18 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +4 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +10 -5
- package/dist/cross-instance-envelope.js +20 -17
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +90 -63
- package/dist/daemon.js +412 -317
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.d.ts +8 -1
- package/dist/daily-summary.js +8 -5
- package/dist/daily-summary.js.map +1 -1
- package/dist/delivery-outbox.d.ts +31 -22
- package/dist/delivery-outbox.js +119 -161
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +10 -25
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/fleet-level-config.js +1 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +4 -21
- package/dist/fleet-manager.js +38 -159
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-service-owner.d.ts +11 -0
- package/dist/fleet-service-owner.js +3 -0
- package/dist/fleet-service-owner.js.map +1 -0
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +4 -5
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +4 -5
- package/dist/instance-lifecycle.js +23 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +6 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-reconcile.js +3 -3
- package/dist/kiro-reconcile.js.map +1 -1
- package/dist/locale.js +8 -52
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you-hub.js +4 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +3 -1
- package/dist/needs-you-live.js +24 -2
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +6 -1
- package/dist/needs-you.js +4 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +1 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +17 -25
- package/dist/scheduler/task-board.js +29 -39
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +13 -14
- package/dist/sd-notify.js +62 -28
- package/dist/sd-notify.js.map +1 -1
- package/dist/settings-api.d.ts +0 -1
- package/dist/settings-api.js +1 -14
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-effect.js +0 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-wizard.d.ts +3 -0
- package/dist/setup-wizard.js +4 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-waker.d.ts +40 -1
- package/dist/task-board-waker.js +110 -9
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -1
- package/dist/tmux-control.js +3 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.js +18 -16
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +2 -0
- package/dist/tmux-read.js +7 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -0
- package/dist/topic-commands.js +2 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +17 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/types.d.ts +7 -10
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/settings-appearance.js +3 -35
- package/dist/ui/settings-registry.js +0 -1
- package/dist/ui/settings-strings.js +4 -4
- package/dist/ui/settings-web-authority.js +4 -3
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/update-check.d.ts +13 -4
- package/dist/update-check.js +17 -4
- package/dist/update-check.js.map +1 -1
- package/dist/web-experimental.d.ts +6 -2
- package/dist/web-experimental.js +8 -4
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-settings-patch.d.ts +0 -1
- package/dist/web-settings-patch.js +3 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +2 -2
- package/dist/working-recovery.d.ts +0 -2
- package/dist/working-recovery.js +2 -23
- package/dist/working-recovery.js.map +1 -1
- package/launcher/agend-install.cjs +7 -1
- package/launcher/fleet-service-owner.cjs +138 -0
- package/package.json +1 -1
- package/dist/task-board-summary.d.ts +0 -101
- package/dist/task-board-summary.js +0 -177
- package/dist/task-board-summary.js.map +0 -1
- package/dist/upgrade-notices.d.ts +0 -17
- package/dist/upgrade-notices.js +0 -93
- package/dist/upgrade-notices.js.map +0 -1
package/dist/daily-summary.d.ts
CHANGED
|
@@ -3,10 +3,17 @@ import type { DailySummaryConfig } from "./types.js";
|
|
|
3
3
|
export declare class DailySummary {
|
|
4
4
|
private config;
|
|
5
5
|
private timezone;
|
|
6
|
+
/** Every day at the configured time: the report's text, or null when the report is off (#2804). */
|
|
6
7
|
private onSummary;
|
|
7
8
|
private getSummaryText;
|
|
8
9
|
private job;
|
|
9
|
-
constructor(config: DailySummaryConfig, timezone: string,
|
|
10
|
+
constructor(config: DailySummaryConfig, timezone: string,
|
|
11
|
+
/** Every day at the configured time: the report's text, or null when the report is off (#2804). */
|
|
12
|
+
onSummary: (text: string | null) => void, getSummaryText: () => string);
|
|
13
|
+
/**
|
|
14
|
+
* #2804: the clock always runs — the daily housekeeping (log and inbox rotation) rides on it — and the report is
|
|
15
|
+
* built and handed on only when `enabled`.
|
|
16
|
+
*/
|
|
10
17
|
start(): void;
|
|
11
18
|
stop(): void;
|
|
12
19
|
static generateText(eventLog: EventLog, instances: string[], costCentsMap: Map<string, number>, fleetTotalCents: number): string;
|
package/dist/daily-summary.js
CHANGED
|
@@ -7,19 +7,22 @@ export class DailySummary {
|
|
|
7
7
|
onSummary;
|
|
8
8
|
getSummaryText;
|
|
9
9
|
job = null;
|
|
10
|
-
constructor(config, timezone,
|
|
10
|
+
constructor(config, timezone,
|
|
11
|
+
/** Every day at the configured time: the report's text, or null when the report is off (#2804). */
|
|
12
|
+
onSummary, getSummaryText) {
|
|
11
13
|
this.config = config;
|
|
12
14
|
this.timezone = timezone;
|
|
13
15
|
this.onSummary = onSummary;
|
|
14
16
|
this.getSummaryText = getSummaryText;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* #2804: the clock always runs — the daily housekeeping (log and inbox rotation) rides on it — and the report is
|
|
20
|
+
* built and handed on only when `enabled`.
|
|
21
|
+
*/
|
|
16
22
|
start() {
|
|
17
|
-
if (!this.config.enabled)
|
|
18
|
-
return;
|
|
19
23
|
const cron = `${this.config.minute} ${this.config.hour} * * *`;
|
|
20
24
|
this.job = new Cron(cron, { timezone: this.timezone }, () => {
|
|
21
|
-
|
|
22
|
-
this.onSummary(text);
|
|
25
|
+
this.onSummary(this.config.enabled ? this.getSummaryText() : null);
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
28
|
stop() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daily-summary.js","sourceRoot":"","sources":["../src/daily-summary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAG9B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE5E,MAAM,OAAO,YAAY;IAIb,MAAM;IACN,QAAQ;
|
|
1
|
+
{"version":3,"file":"daily-summary.js","sourceRoot":"","sources":["../src/daily-summary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAG9B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE5E,MAAM,OAAO,YAAY;IAIb,MAAM;IACN,QAAQ;IAER,SAAS;IACT,cAAc;IAPhB,GAAG,GAAgB,IAAI,CAAC;IAEhC,YACU,MAA0B,EAC1B,QAAgB;IACxB,mGAAmG;IAC3F,SAAwC,EACxC,cAA4B;sBAJ5B,MAAM;wBACN,QAAQ;yBAER,SAAS;8BACT,cAAc;IACrB,CAAC;IAEJ;;;OAGG;IACH,KAAK;QACH,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC;QAC/D,IAAI,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE;YAC1D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI;QACF,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,YAAY,CACjB,QAAkB,EAClB,SAAmB,EACnB,YAAiC,EACjC,eAAuB;QAEvB,0EAA0E;QAC1E,8EAA8E;QAC9E,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,KAAK,WAAW,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3D,MAAM,KAAK,GAAa,CAAC,qBAAqB,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAE3D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC;YACzE,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,eAAe,CAAC,CAAC,MAAM,CAAC;YAClF,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,mBAAmB,CAAC,CAAC,MAAM,CAAC;YACzF,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAE9C,IAAI,IAAI,GAAG,GAAG,IAAI,KAAK,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;YAChD,IAAI,QAAQ,GAAG,CAAC;gBAAE,IAAI,IAAI,KAAK,QAAQ,WAAW,CAAC;YAEnD,MAAM,SAAS,GAAa,EAAE,CAAC;YAC/B,IAAI,KAAK,GAAG,CAAC;gBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,IAAI,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAEhE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,UAAU,WAAW,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;QAErD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -291,6 +291,12 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
291
291
|
};
|
|
292
292
|
hasOtherPendingWork(target: string, deliveryId: string): boolean;
|
|
293
293
|
withdrawWorkingRecovery(deliveryId: string, reason: string): void;
|
|
294
|
+
/**
|
|
295
|
+
* #2801: AgEnD's own notices with a key starting `prefix` that are still waiting, never begun (as #2797: an aborted
|
|
296
|
+
* attempt wrote nothing), and that `stale(sourceKey, createdAt)` says are overtaken, are not delivered — `cancelled`
|
|
297
|
+
* with `reason`, still readable (delivery_status, Fleet traffic). Returns the ids dropped.
|
|
298
|
+
*/
|
|
299
|
+
withdrawWaitingNotices(prefix: string, stale: (sourceKey: string, createdAt: string) => boolean, reason: string): string[];
|
|
294
300
|
private noteWorkingReferences;
|
|
295
301
|
constructor(dbPath: string, managerBootId?: string);
|
|
296
302
|
private initialize;
|
|
@@ -306,34 +312,20 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
306
312
|
/** The owner's message back to the requester on the same correlation_id answers it — at admission. */
|
|
307
313
|
private answerReplyObligation;
|
|
308
314
|
/**
|
|
309
|
-
* #926 safety net 1: remind an owner that has not answered
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
315
|
+
* #926 safety net 1: remind an owner that has not answered, once per ask, after the grace (a reply seconds after the
|
|
316
|
+
* ask is normal). Marked in the same transaction as the notice. #2802: the only reply reminder — the per-boot restart
|
|
317
|
+
* batch is gone (an owner idle after a restart gets this one, per ask, unless it was already reminded), and so are the
|
|
318
|
+
* requester's overdue and expiry notices.
|
|
313
319
|
*/
|
|
314
320
|
remindReplyObligations(ownerInstance: string, opts: {
|
|
315
321
|
now?: Date;
|
|
316
322
|
graceMs: number;
|
|
317
|
-
reason: "turn-ended" | "restart";
|
|
318
|
-
since?: Date;
|
|
319
|
-
restartId?: string;
|
|
320
323
|
}): number;
|
|
321
|
-
|
|
322
|
-
/** Expiry is terminal for this ask; a later delivered re-ask can reopen the same tuple. */
|
|
324
|
+
/** Expiry is terminal for this ask; a later delivered re-ask can reopen the same tuple. #2802: logged, no notice. */
|
|
323
325
|
expireReplyObligations(opts?: {
|
|
324
326
|
now?: Date;
|
|
325
327
|
ttlMs?: number;
|
|
326
328
|
}): ReplyObligation[];
|
|
327
|
-
/**
|
|
328
|
-
* #926 safety net 2 (poll-on-timeout, in code): tell the requester once when
|
|
329
|
-
* the owner is not working and nothing has come back for `overdueMs` since
|
|
330
|
-
* the last ask or reminder. A still-working owner is never reported.
|
|
331
|
-
*/
|
|
332
|
-
notifyOverdueReplyObligations(opts: {
|
|
333
|
-
now?: Date;
|
|
334
|
-
overdueMs: number;
|
|
335
|
-
ownerIdle: (owner: string) => boolean;
|
|
336
|
-
}): number;
|
|
337
329
|
/** A system notice as a durable row, like #929's outcome notices; the key makes it idempotent. */
|
|
338
330
|
private insertReplyObligationNotice;
|
|
339
331
|
/**
|
|
@@ -411,11 +403,31 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
411
403
|
* proof), or does not exist. A write error throws; nothing is remembered as acknowledged elsewhere.
|
|
412
404
|
*/
|
|
413
405
|
acknowledge(deliveryId: string, by: string, atIso?: string): boolean;
|
|
406
|
+
/**
|
|
407
|
+
* #2785 (c): the texts `source` sent `target` (fleet_inbound / steer) admitted at or after `sinceIso`, newest first,
|
|
408
|
+
* looked for among the target's newest `scan` rows only. The rows are found by the target index alone and read one by
|
|
409
|
+
* one: a filter on source or time over the table would read every row's payload (both columns follow it).
|
|
410
|
+
*/
|
|
411
|
+
recentTextsFrom(target: string, source: string, sinceIso: string, scan?: number): string[];
|
|
414
412
|
countForTarget(target: string, states: readonly OutboxState[]): number;
|
|
415
413
|
nextRetryAt(): string | null;
|
|
416
414
|
/** Claim the earliest eligible target row; one caller owns each target lane. */
|
|
417
415
|
claimNext(managerBootId: string, targetBootIdFor: (target: string) => string | null, blockedTargets: ReadonlySet<string>, eligible?: (delivery: OutboxDelivery) => boolean): ClaimedOutboxDelivery | undefined;
|
|
418
416
|
private claimNextSync;
|
|
417
|
+
/**
|
|
418
|
+
* #2785 (b): a message still waiting while a newer one from the same sender to the same target on the same thread
|
|
419
|
+
* (correlation_id) waits behind it is not delivered — the newer one replaces it (a review round for an older PR head, a
|
|
420
|
+
* CI result since overtaken). Only agents' messages (fleet_inbound, steer) that never began: a row with any attempt
|
|
421
|
+
* other than an aborted one keeps its place. The dropped row is `cancelled`, its reason naming the newer delivery, and
|
|
422
|
+
* stays readable (delivery_status, Fleet traffic). A reply obligation opens only on delivery, from the delivered row's own
|
|
423
|
+
* payload: a row that requires a reply is replaced only by one that requires one too, else it keeps its place (#2797
|
|
424
|
+
* review — an update without requires_reply must not swallow the request it follows).
|
|
425
|
+
*
|
|
426
|
+
* #2815: a silent schedule's ticks are one thread per schedule. Each tick is a `raw_paste` with its own correlation id
|
|
427
|
+
* (`schedule:<id>:<run>`), so ticks queued while the target was stuck were all pasted back to back once it was free;
|
|
428
|
+
* now a newer tick of the same schedule replaces the older ones that never began, by the same rule.
|
|
429
|
+
*/
|
|
430
|
+
private supersedeThreadRows;
|
|
419
431
|
/** Idempotent begin permit, committed immediately before the pane side effect. */
|
|
420
432
|
begin(deliveryId: string, targetBootId: string, attemptNo: number, evidence?: DeliveryAttemptEvidence): "begun" | "duplicate" | "stale";
|
|
421
433
|
/** Write-ahead fence: commit before tmux is allowed to receive Enter. */
|
|
@@ -453,8 +465,6 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
453
465
|
queued: number;
|
|
454
466
|
reconciliationPending: number;
|
|
455
467
|
};
|
|
456
|
-
/** Atomically create a post-restart notice for an accepted operation whose response was not recorded. */
|
|
457
|
-
admitPostRestartOutcomeNotice(parent: OutboxDelivery, currentSourceBootId: string): OutboxDelivery | undefined;
|
|
458
468
|
recoverTargetGeneration(targetInstance: string, currentTargetBootId: string): {
|
|
459
469
|
queued: number;
|
|
460
470
|
reconciliationPending: number;
|
|
@@ -464,7 +474,6 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
464
474
|
getReconciliationCandidates(targetInstance: string): DeliveryReconciliationCandidate[];
|
|
465
475
|
/** Atomically settle one old attempt using only its boot/attempt fence. */
|
|
466
476
|
reconcileAttempt(deliveryId: string, targetBootId: string, attemptNo: number, outcome: "delivered" | "retry_wait" | "uncertain", evidence: string): boolean;
|
|
467
|
-
getUnansweredAccepted(sourceInstance: string, currentBootId: string): OutboxDelivery[];
|
|
468
477
|
close(): void;
|
|
469
478
|
/** False once the database connection is closed: callers on timers check it before reading. */
|
|
470
479
|
get isOpen(): boolean;
|
package/dist/delivery-outbox.js
CHANGED
|
@@ -13,18 +13,6 @@ export const DURABLE_DELIVERY_MAX_ATTEMPTS = 8;
|
|
|
13
13
|
export const DURABLE_DELIVERY_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
14
14
|
export const DURABLE_DELIVERY_ABORT_BACKOFF_MS = 5_000;
|
|
15
15
|
export const DEFAULT_REPLY_OBLIGATION_TTL_MS = 24 * 60 * 60_000;
|
|
16
|
-
const RESTART_REPLY_DISPLAY_LIMIT = 10;
|
|
17
|
-
/** Keep each preview on one physical line and bound even an arbitrary correlation ID. */
|
|
18
|
-
function replyPreview(value) {
|
|
19
|
-
const escaped = value.replace(/[\u0000-\u001f\u007f]/g, c => JSON.stringify(c).slice(1, -1));
|
|
20
|
-
const bytes = Buffer.from(escaped);
|
|
21
|
-
if (bytes.length <= 256)
|
|
22
|
-
return escaped;
|
|
23
|
-
let end = 253;
|
|
24
|
-
while ((bytes[end] & 0xc0) === 0x80)
|
|
25
|
-
end--;
|
|
26
|
-
return bytes.subarray(0, end).toString("utf8") + "…";
|
|
27
|
-
}
|
|
28
16
|
/** The one mapping from validated tool/CLI arguments to a selector. */
|
|
29
17
|
export function deliveryStatusSelector(args) {
|
|
30
18
|
const page = { ...(args.limit !== undefined ? { limit: args.limit } : {}), ...(args.cursor ? { cursor: args.cursor } : {}) };
|
|
@@ -66,6 +54,15 @@ function payloadRequiresReply(payload) {
|
|
|
66
54
|
const meta = payload && typeof payload === "object" ? payload.meta : undefined;
|
|
67
55
|
return !!meta && typeof meta === "object" && meta.requires_reply === "true";
|
|
68
56
|
}
|
|
57
|
+
/** Whether a row's payload asks for a reply (an unreadable payload: no). */
|
|
58
|
+
function rowRequiresReply(row) {
|
|
59
|
+
try {
|
|
60
|
+
return payloadRequiresReply(JSON.parse(row.payload_json));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
69
66
|
function payloadMessageId(payload) {
|
|
70
67
|
const meta = payload && typeof payload === "object" ? payload.meta : undefined;
|
|
71
68
|
const id = meta && typeof meta === "object" ? meta.message_id : undefined;
|
|
@@ -354,6 +351,27 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
354
351
|
if (changed)
|
|
355
352
|
this.emit("state", { deliveryId, state: "cancelled" });
|
|
356
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* #2801: AgEnD's own notices with a key starting `prefix` that are still waiting, never begun (as #2797: an aborted
|
|
356
|
+
* attempt wrote nothing), and that `stale(sourceKey, createdAt)` says are overtaken, are not delivered — `cancelled`
|
|
357
|
+
* with `reason`, still readable (delivery_status, Fleet traffic). Returns the ids dropped.
|
|
358
|
+
*/
|
|
359
|
+
withdrawWaitingNotices(prefix, stale, reason) {
|
|
360
|
+
const rows = this.db.prepare(`SELECT delivery_id, source_key, created_at FROM deliveries
|
|
361
|
+
WHERE kind=? AND state IN ('queued','retry_wait') AND substr(source_key,1,?)=? ORDER BY created_seq`)
|
|
362
|
+
.all(SYSTEM_NOTICE_KIND, prefix.length, prefix);
|
|
363
|
+
const picked = rows.filter((r) => stale(r.source_key, r.created_at));
|
|
364
|
+
if (!picked.length)
|
|
365
|
+
return [];
|
|
366
|
+
const begun = this.db.prepare("SELECT 1 FROM delivery_attempts WHERE delivery_id=? AND state<>'aborted' LIMIT 1");
|
|
367
|
+
const now = new Date().toISOString();
|
|
368
|
+
const cancel = this.db.prepare(`UPDATE deliveries SET state='cancelled',finished_at=?,updated_at=?,last_error=?,next_attempt_at=NULL
|
|
369
|
+
WHERE delivery_id=? AND state IN ('queued','retry_wait')`);
|
|
370
|
+
const done = this.db.transaction(() => picked.filter((r) => !begun.get(r.delivery_id) && cancel.run(now, now, reason, r.delivery_id).changes === 1).map((r) => r.delivery_id))();
|
|
371
|
+
for (const deliveryId of done)
|
|
372
|
+
this.emit("state", { deliveryId, state: "cancelled" });
|
|
373
|
+
return done;
|
|
374
|
+
}
|
|
357
375
|
noteWorkingReferences(instances) {
|
|
358
376
|
// The transaction has committed. Telemetry must not change its result.
|
|
359
377
|
try {
|
|
@@ -470,8 +488,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
470
488
|
PRIMARY KEY (correlation_id, requester_instance, owner_instance)
|
|
471
489
|
);
|
|
472
490
|
CREATE INDEX IF NOT EXISTS idx_reply_obligation_owner_state ON reply_obligations(owner_instance, state);
|
|
473
|
-
--
|
|
474
|
-
-- independently: no notice FK/cascade and no request or message content.
|
|
491
|
+
-- #2802: no longer written (the per-boot restart reminder is gone); kept so existing databases keep their schema.
|
|
475
492
|
CREATE TABLE IF NOT EXISTS reply_restart_markers (
|
|
476
493
|
owner_instance TEXT NOT NULL,
|
|
477
494
|
restart_id TEXT NOT NULL,
|
|
@@ -635,47 +652,16 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
635
652
|
`).run(now, answer.delivery_id, answer.correlation_id, answer.source_instance, answer.target_instance);
|
|
636
653
|
}
|
|
637
654
|
/**
|
|
638
|
-
* #926 safety net 1: remind an owner that has not answered
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
655
|
+
* #926 safety net 1: remind an owner that has not answered, once per ask, after the grace (a reply seconds after the
|
|
656
|
+
* ask is normal). Marked in the same transaction as the notice. #2802: the only reply reminder — the per-boot restart
|
|
657
|
+
* batch is gone (an owner idle after a restart gets this one, per ask, unless it was already reminded), and so are the
|
|
658
|
+
* requester's overdue and expiry notices.
|
|
642
659
|
*/
|
|
643
660
|
remindReplyObligations(ownerInstance, opts) {
|
|
644
661
|
const now = opts.now ?? new Date();
|
|
645
662
|
const nowIso = now.toISOString();
|
|
646
663
|
const graceCutoff = new Date(now.getTime() - opts.graceMs).toISOString();
|
|
647
|
-
const sinceIso = (opts.since ?? now).toISOString();
|
|
648
664
|
const inserted = this.db.transaction(() => {
|
|
649
|
-
if (opts.reason === "restart") {
|
|
650
|
-
const restartId = opts.restartId ?? sinceIso;
|
|
651
|
-
const key = `reply-pending:restart:${JSON.stringify([ownerInstance, restartId])}`;
|
|
652
|
-
if (this.db.prepare("SELECT 1 FROM reply_restart_markers WHERE owner_instance=? AND restart_id=?")
|
|
653
|
-
.get(ownerInstance, restartId))
|
|
654
|
-
return 0;
|
|
655
|
-
// Adopt an existing notice from the earlier implementation/rollback
|
|
656
|
-
// without stamping a new ask or producing another notice.
|
|
657
|
-
const prior = this.db.prepare("SELECT created_at FROM deliveries WHERE source_key=?").get(key);
|
|
658
|
-
if (prior) {
|
|
659
|
-
this.recordReplyRestart(ownerInstance, restartId, prior.created_at);
|
|
660
|
-
return 0;
|
|
661
|
-
}
|
|
662
|
-
const total = this.db.prepare("SELECT count(*) n FROM reply_obligations WHERE owner_instance=? AND state='open'")
|
|
663
|
-
.get(ownerInstance).n;
|
|
664
|
-
if (!total)
|
|
665
|
-
return 0;
|
|
666
|
-
const preview = this.db.prepare(`SELECT requester_instance,correlation_id,last_asked_at FROM reply_obligations
|
|
667
|
-
WHERE owner_instance=? AND state='open' ORDER BY last_asked_at,correlation_id,requester_instance LIMIT ?`)
|
|
668
|
-
.all(ownerInstance, RESTART_REPLY_DISPLAY_LIMIT);
|
|
669
|
-
const lines = preview.map(row => `${replyPreview(row.requester_instance)}; correlation_id=${replyPreview(row.correlation_id)}; asked=${replyPreview(row.last_asked_at)}`);
|
|
670
|
-
if (total > preview.length)
|
|
671
|
-
lines.push(t("delivery.reply_restart_more", total - preview.length));
|
|
672
|
-
const content = t("delivery.reply_restart") + "\n" + lines.join("\n");
|
|
673
|
-
if (!this.insertReplyObligationNotice(ownerInstance, null, key, content, nowIso))
|
|
674
|
-
return 0;
|
|
675
|
-
this.recordReplyRestart(ownerInstance, restartId, nowIso);
|
|
676
|
-
this.db.prepare("UPDATE reply_obligations SET nudged_at=? WHERE owner_instance=? AND state='open'").run(nowIso, ownerInstance);
|
|
677
|
-
return 1;
|
|
678
|
-
}
|
|
679
665
|
const due = this.db.prepare("SELECT * FROM reply_obligations WHERE owner_instance=? AND state='open' AND nudged_at IS NULL AND last_asked_at <= ?")
|
|
680
666
|
.all(ownerInstance, graceCutoff);
|
|
681
667
|
let count = 0;
|
|
@@ -694,11 +680,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
694
680
|
this.emit("admitted");
|
|
695
681
|
return inserted;
|
|
696
682
|
}
|
|
697
|
-
|
|
698
|
-
this.db.prepare("INSERT INTO reply_restart_markers(owner_instance,restart_id,reminded_at) VALUES (?,?,?)")
|
|
699
|
-
.run(owner, restartId, now);
|
|
700
|
-
}
|
|
701
|
-
/** Expiry is terminal for this ask; a later delivered re-ask can reopen the same tuple. */
|
|
683
|
+
/** Expiry is terminal for this ask; a later delivered re-ask can reopen the same tuple. #2802: logged, no notice. */
|
|
702
684
|
expireReplyObligations(opts = {}) {
|
|
703
685
|
const now = opts.now ?? new Date();
|
|
704
686
|
const ttl = opts.ttlMs !== undefined && Number.isFinite(opts.ttlMs) && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_REPLY_OBLIGATION_TTL_MS;
|
|
@@ -708,58 +690,19 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
708
690
|
const due = this.db.prepare("SELECT * FROM reply_obligations WHERE state='open' AND last_asked_at<=? ORDER BY last_asked_at")
|
|
709
691
|
.all(cutoff);
|
|
710
692
|
for (const row of due) {
|
|
711
|
-
|
|
712
|
-
const key = `reply-overdue:${row.correlation_id}:${row.requester_instance}:${row.owner_instance}:${row.last_asked_at}`;
|
|
713
|
-
this.insertReplyObligationNotice(row.requester_instance, row.correlation_id, key, t("delivery.reply_expired", row.owner_instance, row.correlation_id, row.last_asked_at), nowIso);
|
|
714
|
-
}
|
|
715
|
-
this.db.prepare(`UPDATE reply_obligations SET state='expired',expired_at=?,overdue_notified_at=COALESCE(overdue_notified_at,?)
|
|
693
|
+
this.db.prepare(`UPDATE reply_obligations SET state='expired',expired_at=?
|
|
716
694
|
WHERE correlation_id=? AND requester_instance=? AND owner_instance=? AND state='open'`)
|
|
717
|
-
.run(nowIso,
|
|
695
|
+
.run(nowIso, row.correlation_id, row.requester_instance, row.owner_instance);
|
|
718
696
|
row.state = "expired";
|
|
719
697
|
row.expired_at = nowIso;
|
|
720
|
-
row.overdue_notified_at ??= nowIso;
|
|
721
698
|
}
|
|
722
699
|
return due.map(mapObligation);
|
|
723
700
|
})();
|
|
724
701
|
if (expired.length) {
|
|
725
|
-
this.emit("admitted");
|
|
726
702
|
this.noteWorkingReferences(expired.flatMap(row => [row.ownerInstance, row.requesterInstance]));
|
|
727
703
|
}
|
|
728
704
|
return expired;
|
|
729
705
|
}
|
|
730
|
-
/**
|
|
731
|
-
* #926 safety net 2 (poll-on-timeout, in code): tell the requester once when
|
|
732
|
-
* the owner is not working and nothing has come back for `overdueMs` since
|
|
733
|
-
* the last ask or reminder. A still-working owner is never reported.
|
|
734
|
-
*/
|
|
735
|
-
notifyOverdueReplyObligations(opts) {
|
|
736
|
-
if (!(opts.overdueMs > 0))
|
|
737
|
-
return 0;
|
|
738
|
-
const now = opts.now ?? new Date();
|
|
739
|
-
const nowIso = now.toISOString();
|
|
740
|
-
const inserted = this.db.transaction(() => {
|
|
741
|
-
const open = this.db.prepare(`
|
|
742
|
-
SELECT * FROM reply_obligations WHERE state='open' AND overdue_notified_at IS NULL ORDER BY opened_at
|
|
743
|
-
`).all();
|
|
744
|
-
let count = 0;
|
|
745
|
-
for (const row of open) {
|
|
746
|
-
const quietSince = Math.max(Date.parse(row.last_asked_at), row.nudged_at ? Date.parse(row.nudged_at) : 0);
|
|
747
|
-
if (now.getTime() - quietSince < opts.overdueMs || !opts.ownerIdle(row.owner_instance))
|
|
748
|
-
continue;
|
|
749
|
-
const content = t("delivery.reply_overdue", row.owner_instance, row.correlation_id, row.last_asked_at, row.nudged_at ? t("delivery.reply_reminded", row.nudged_at) : "");
|
|
750
|
-
const key = `reply-overdue:${row.correlation_id}:${row.requester_instance}:${row.owner_instance}:${row.last_asked_at}`;
|
|
751
|
-
if (this.insertReplyObligationNotice(row.requester_instance, row.correlation_id, key, content, nowIso))
|
|
752
|
-
count++;
|
|
753
|
-
this.db.prepare(`
|
|
754
|
-
UPDATE reply_obligations SET overdue_notified_at=? WHERE correlation_id=? AND requester_instance=? AND owner_instance=? AND state='open'
|
|
755
|
-
`).run(nowIso, row.correlation_id, row.requester_instance, row.owner_instance);
|
|
756
|
-
}
|
|
757
|
-
return count;
|
|
758
|
-
})();
|
|
759
|
-
if (inserted > 0)
|
|
760
|
-
this.emit("admitted");
|
|
761
|
-
return inserted;
|
|
762
|
-
}
|
|
763
706
|
/** A system notice as a durable row, like #929's outcome notices; the key makes it idempotent. */
|
|
764
707
|
insertReplyObligationNotice(target, correlationId, sourceKey, content, now) {
|
|
765
708
|
const noticeId = randomUUID();
|
|
@@ -1089,6 +1032,31 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1089
1032
|
`).run(atIso, by, deliveryId);
|
|
1090
1033
|
return result.changes === 1;
|
|
1091
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* #2785 (c): the texts `source` sent `target` (fleet_inbound / steer) admitted at or after `sinceIso`, newest first,
|
|
1037
|
+
* looked for among the target's newest `scan` rows only. The rows are found by the target index alone and read one by
|
|
1038
|
+
* one: a filter on source or time over the table would read every row's payload (both columns follow it).
|
|
1039
|
+
*/
|
|
1040
|
+
recentTextsFrom(target, source, sinceIso, scan = 30) {
|
|
1041
|
+
const rows = this.db.prepare("SELECT rowid AS r FROM deliveries WHERE target_instance=? ORDER BY created_seq DESC LIMIT ?")
|
|
1042
|
+
.all(target, scan);
|
|
1043
|
+
const read = this.db.prepare("SELECT source_instance, kind, created_at, payload_json FROM deliveries WHERE rowid=?");
|
|
1044
|
+
const texts = [];
|
|
1045
|
+
for (const { r } of rows) {
|
|
1046
|
+
const row = read.get(r);
|
|
1047
|
+
if (!row || row.created_at < sinceIso)
|
|
1048
|
+
continue;
|
|
1049
|
+
if (row.source_instance !== source || (row.kind !== "fleet_inbound" && row.kind !== "steer"))
|
|
1050
|
+
continue;
|
|
1051
|
+
try {
|
|
1052
|
+
const content = JSON.parse(row.payload_json).content;
|
|
1053
|
+
if (typeof content === "string")
|
|
1054
|
+
texts.push(content);
|
|
1055
|
+
}
|
|
1056
|
+
catch { /* an unreadable payload says nothing */ }
|
|
1057
|
+
}
|
|
1058
|
+
return texts;
|
|
1059
|
+
}
|
|
1092
1060
|
countForTarget(target, states) {
|
|
1093
1061
|
if (states.length === 0)
|
|
1094
1062
|
return 0;
|
|
@@ -1111,10 +1079,12 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1111
1079
|
}
|
|
1112
1080
|
claimNextSync(managerBootId, targetBootIdFor, blockedTargets, eligible) {
|
|
1113
1081
|
const now = new Date().toISOString();
|
|
1114
|
-
const
|
|
1082
|
+
const pending = this.db.prepare(`
|
|
1115
1083
|
SELECT * FROM deliveries WHERE state IN ('queued','retry_wait')
|
|
1116
1084
|
ORDER BY created_seq
|
|
1117
1085
|
`).all();
|
|
1086
|
+
const superseded = this.supersedeThreadRows(pending, now);
|
|
1087
|
+
const rows = superseded.size ? pending.filter(row => !superseded.has(row.delivery_id)) : pending;
|
|
1118
1088
|
// The oldest pending row owns its target lane even while it is in retry
|
|
1119
1089
|
// backoff or the target is temporarily unavailable. Later rows must not
|
|
1120
1090
|
// pass it merely because they happen to be eligible first.
|
|
@@ -1153,6 +1123,58 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1153
1123
|
}
|
|
1154
1124
|
return undefined;
|
|
1155
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* #2785 (b): a message still waiting while a newer one from the same sender to the same target on the same thread
|
|
1128
|
+
* (correlation_id) waits behind it is not delivered — the newer one replaces it (a review round for an older PR head, a
|
|
1129
|
+
* CI result since overtaken). Only agents' messages (fleet_inbound, steer) that never began: a row with any attempt
|
|
1130
|
+
* other than an aborted one keeps its place. The dropped row is `cancelled`, its reason naming the newer delivery, and
|
|
1131
|
+
* stays readable (delivery_status, Fleet traffic). A reply obligation opens only on delivery, from the delivered row's own
|
|
1132
|
+
* payload: a row that requires a reply is replaced only by one that requires one too, else it keeps its place (#2797
|
|
1133
|
+
* review — an update without requires_reply must not swallow the request it follows).
|
|
1134
|
+
*
|
|
1135
|
+
* #2815: a silent schedule's ticks are one thread per schedule. Each tick is a `raw_paste` with its own correlation id
|
|
1136
|
+
* (`schedule:<id>:<run>`), so ticks queued while the target was stuck were all pasted back to back once it was free;
|
|
1137
|
+
* now a newer tick of the same schedule replaces the older ones that never began, by the same rule.
|
|
1138
|
+
*/
|
|
1139
|
+
supersedeThreadRows(pending, now) {
|
|
1140
|
+
const newest = new Map();
|
|
1141
|
+
const keyOf = (row) => {
|
|
1142
|
+
if (!row.correlation_id)
|
|
1143
|
+
return null;
|
|
1144
|
+
if (row.kind === "fleet_inbound" || row.kind === "steer")
|
|
1145
|
+
return `${row.source_instance}\u0000${row.target_instance}\u0000${row.correlation_id}`;
|
|
1146
|
+
const schedule = row.kind === "raw_paste" ? /^schedule:([^:]+):/.exec(row.correlation_id)?.[1] : undefined;
|
|
1147
|
+
return schedule ? `schedule\u0000${row.source_instance}\u0000${row.target_instance}\u0000${schedule}` : null;
|
|
1148
|
+
};
|
|
1149
|
+
for (const row of pending) {
|
|
1150
|
+
const key = keyOf(row);
|
|
1151
|
+
if (key)
|
|
1152
|
+
newest.set(key, row);
|
|
1153
|
+
} // ascending: the last wins
|
|
1154
|
+
const candidates = pending.filter(row => { const key = keyOf(row); return key !== null && newest.get(key).delivery_id !== row.delivery_id; });
|
|
1155
|
+
if (!candidates.length)
|
|
1156
|
+
return new Set();
|
|
1157
|
+
const begun = this.db.prepare("SELECT 1 FROM delivery_attempts WHERE delivery_id=? AND state<>'aborted' LIMIT 1");
|
|
1158
|
+
const cancel = this.db.prepare(`UPDATE deliveries SET state='cancelled',finished_at=?,updated_at=?,last_error=?,next_attempt_at=NULL
|
|
1159
|
+
WHERE delivery_id=? AND state IN ('queued','retry_wait')`);
|
|
1160
|
+
const done = this.db.transaction(() => {
|
|
1161
|
+
const out = [];
|
|
1162
|
+
for (const row of candidates) {
|
|
1163
|
+
if (begun.get(row.delivery_id))
|
|
1164
|
+
continue;
|
|
1165
|
+
const newer = newest.get(keyOf(row));
|
|
1166
|
+
if (rowRequiresReply(row) && !rowRequiresReply(newer))
|
|
1167
|
+
continue;
|
|
1168
|
+
const by = newer.delivery_id;
|
|
1169
|
+
if (cancel.run(now, now, `superseded by ${by}`, row.delivery_id).changes === 1)
|
|
1170
|
+
out.push(row.delivery_id);
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
})();
|
|
1174
|
+
for (const deliveryId of done)
|
|
1175
|
+
this.emit("state", { deliveryId, state: "cancelled" });
|
|
1176
|
+
return new Set(done);
|
|
1177
|
+
}
|
|
1156
1178
|
/** Idempotent begin permit, committed immediately before the pane side effect. */
|
|
1157
1179
|
begin(deliveryId, targetBootId, attemptNo, evidence) {
|
|
1158
1180
|
const now = new Date().toISOString();
|
|
@@ -1508,60 +1530,6 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1508
1530
|
});
|
|
1509
1531
|
return transaction();
|
|
1510
1532
|
}
|
|
1511
|
-
/** Atomically create a post-restart notice for an accepted operation whose response was not recorded. */
|
|
1512
|
-
admitPostRestartOutcomeNotice(parent, currentSourceBootId) {
|
|
1513
|
-
const sourceKey = `post-restart:${parent.sourceInstance}:${parent.sourceDaemonBootId}:${parent.operationId}:${parent.targetInstance}`;
|
|
1514
|
-
const operationId = `notice:${parent.sourceDaemonBootId}:${parent.operationId}:${parent.targetInstance}`;
|
|
1515
|
-
const now = new Date().toISOString();
|
|
1516
|
-
const deliveryId = randomUUID();
|
|
1517
|
-
const noticeText = t("delivery.post_restart", parent.targetInstance, statusSummary(parent.state), parent.operationId, parent.state);
|
|
1518
|
-
const payload = {
|
|
1519
|
-
type: "fleet_inbound",
|
|
1520
|
-
content: noticeText,
|
|
1521
|
-
meta: {
|
|
1522
|
-
user: "AgEnD recovery",
|
|
1523
|
-
user_id: "agend-system",
|
|
1524
|
-
message_id: `delivery-notice-${parent.operationId}-${parent.targetInstance}`,
|
|
1525
|
-
chat_id: "",
|
|
1526
|
-
thread_id: "",
|
|
1527
|
-
source: "delivery-outbox",
|
|
1528
|
-
},
|
|
1529
|
-
};
|
|
1530
|
-
const transaction = this.db.transaction(() => {
|
|
1531
|
-
const prior = this.db.prepare(`
|
|
1532
|
-
SELECT d.* FROM outcome_notices n JOIN deliveries d ON d.delivery_id=n.notice_delivery_id
|
|
1533
|
-
WHERE n.source_instance=? AND n.source_daemon_boot_id=? AND n.operation_id=? AND n.target_instance=?
|
|
1534
|
-
`).get(parent.sourceInstance, parent.sourceDaemonBootId, parent.operationId, parent.targetInstance);
|
|
1535
|
-
if (prior)
|
|
1536
|
-
return mapRow(prior);
|
|
1537
|
-
const terminalNotice = this.db.prepare(`
|
|
1538
|
-
SELECT d.* FROM failure_notices n JOIN deliveries d ON d.delivery_id=n.notice_delivery_id
|
|
1539
|
-
WHERE n.parent_delivery_id=?
|
|
1540
|
-
`).get(parent.deliveryId);
|
|
1541
|
-
if (terminalNotice)
|
|
1542
|
-
return mapRow(terminalNotice);
|
|
1543
|
-
const reserved = this.db.prepare(`
|
|
1544
|
-
INSERT OR IGNORE INTO outcome_notices
|
|
1545
|
-
(source_instance,source_daemon_boot_id,operation_id,target_instance,notice_delivery_id,created_at)
|
|
1546
|
-
VALUES (?,?,?,?,?,?)
|
|
1547
|
-
`).run(parent.sourceInstance, parent.sourceDaemonBootId, parent.operationId, parent.targetInstance, deliveryId, now);
|
|
1548
|
-
if (reserved.changes !== 1)
|
|
1549
|
-
return undefined;
|
|
1550
|
-
this.db.prepare(`
|
|
1551
|
-
INSERT INTO deliveries (
|
|
1552
|
-
delivery_id,operation_id,source_key,source_instance,source_daemon_boot_id,
|
|
1553
|
-
target_instance,target_session,kind,correlation_id,payload_json,state,
|
|
1554
|
-
attempt_no,created_seq,manager_boot_id,created_at,updated_at,accepted_at
|
|
1555
|
-
) VALUES (?,?,?,?,?,?,NULL,'post_restart_outcome_notice',NULL,?,'queued',0,
|
|
1556
|
-
(SELECT COALESCE(MAX(created_seq),0)+1 FROM deliveries),NULL,?,?,?)
|
|
1557
|
-
`).run(deliveryId, operationId, sourceKey, "agend-system", currentSourceBootId, parent.sourceInstance, JSON.stringify(payload), now, now, now);
|
|
1558
|
-
return mapRow(this.db.prepare("SELECT * FROM deliveries WHERE delivery_id=?").get(deliveryId));
|
|
1559
|
-
});
|
|
1560
|
-
const notice = transaction();
|
|
1561
|
-
if (notice)
|
|
1562
|
-
this.emit("admitted", notice);
|
|
1563
|
-
return notice;
|
|
1564
|
-
}
|
|
1565
1533
|
recoverTargetGeneration(targetInstance, currentTargetBootId) {
|
|
1566
1534
|
const now = new Date().toISOString();
|
|
1567
1535
|
const transaction = this.db.transaction(() => {
|
|
@@ -1655,17 +1623,6 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1655
1623
|
this.emit("state", { deliveryId, state: outcome });
|
|
1656
1624
|
return changed;
|
|
1657
1625
|
}
|
|
1658
|
-
getUnansweredAccepted(sourceInstance, currentBootId) {
|
|
1659
|
-
const rows = this.db.prepare(`
|
|
1660
|
-
SELECT * FROM deliveries
|
|
1661
|
-
WHERE source_instance=? AND source_daemon_boot_id<>? AND response_delivered_at IS NULL
|
|
1662
|
-
-- Scheduler admission has no waiting source-agent tool response; its
|
|
1663
|
-
-- stable run key and durable row are the recovery/lookup contract.
|
|
1664
|
-
AND kind NOT IN ('raw_paste')
|
|
1665
|
-
ORDER BY created_seq
|
|
1666
|
-
`).all(sourceInstance, currentBootId);
|
|
1667
|
-
return rows.map(mapRow);
|
|
1668
|
-
}
|
|
1669
1626
|
close() {
|
|
1670
1627
|
this.db.close();
|
|
1671
1628
|
}
|
|
@@ -1796,6 +1753,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
1796
1753
|
}
|
|
1797
1754
|
/** Must be called inside the same SQLite transaction as the terminal transition. */
|
|
1798
1755
|
insertFailureNotice(parent, outcome, reason, now) {
|
|
1756
|
+
// post_restart_outcome_notice: no longer written (#2802), but a row from before may still be queued.
|
|
1799
1757
|
if (parent.kind === "delivery_outcome_notice" || parent.kind === "post_restart_outcome_notice"
|
|
1800
1758
|
|| parent.kind === REPLY_OBLIGATION_NOTICE_KIND || parent.kind === SYSTEM_NOTICE_KIND || parent.kind === WORKING_RECOVERY_KIND)
|
|
1801
1759
|
return;
|