@staix/agent-hub 0.12.29 → 0.12.31
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/CHANGELOG.md +39 -0
- package/README.md +92 -218
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +3 -2
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +7 -1
- package/docs/agent-notes/daemon.md +14 -1
- package/docs/agent-notes/local-worker.md +4 -2
- package/docs/agent-notes/tasks.md +14 -0
- package/docs/agent-notes/tests.md +6 -3
- package/docs/memory.md +70 -152
- package/docs/operations.md +317 -25
- package/docs/plans/2026-10-16-amem-failure-registry.md +101 -0
- package/docs/plans/2026-10-16-amem-p0-baseline.md +110 -0
- package/docs/plans/2026-10-16-amem-p2-read-tools.md +28 -0
- package/docs/plans/amem-cases.json +482 -0
- package/docs/quickstart.md +24 -2
- package/docs/security.md +42 -2
- package/docs/smoke.md +11 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +142 -7
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +84 -10
- package/docs/verification/2026-10-17-0.12.30.md +70 -0
- package/docs/verification/2026-10-17-0.12.31.md +71 -0
- package/docs/verification/2026-10-17-418-probes.md +21 -0
- package/docs/verified.json +13 -13
- package/package.json +5 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +129 -62
- package/src/adapters/claude-channel.ts +29 -11
- package/src/adapters/local-worker.ts +2 -2
- package/src/adapters/pi.ts +5 -0
- package/src/cli/attention.ts +5 -1
- package/src/cli/console-state.ts +145 -29
- package/src/cli/console.ts +40 -10
- package/src/cli/help.ts +3 -2
- package/src/cli/identity.ts +4 -3
- package/src/cli/jcode-mcp.ts +1 -1
- package/src/cli/main.js +2 -2
- package/src/cli/main.ts +43 -14
- package/src/cli/memory.ts +87 -187
- package/src/cli/output.ts +19 -2
- package/src/cli/recovery-package.ts +59 -10
- package/src/cli/upgrade-interactive.ts +2 -1
- package/src/cli/upgrade-restore-state.ts +133 -0
- package/src/cli/upgrade-runtime.ts +204 -63
- package/src/cli/upgrade.ts +57 -24
- package/src/hub/board.ts +83 -24
- package/src/hub/bus.ts +108 -38
- package/src/hub/check-runner.ts +514 -0
- package/src/hub/child-process.ts +1 -0
- package/src/hub/control-client.ts +5 -1
- package/src/hub/daemon.ts +529 -89
- package/src/hub/delivery-hints.ts +30 -0
- package/src/hub/delivery-journal.ts +185 -12
- package/src/hub/delivery-superseded.ts +52 -0
- package/src/hub/envelope.ts +31 -1
- package/src/hub/git.ts +2 -38
- package/src/hub/host-executable.ts +11 -0
- package/src/hub/hub-tools.ts +14 -2
- package/src/hub/manager.ts +1 -1
- package/src/hub/memory-candidates.ts +103 -0
- package/src/hub/memory-fail.ts +25 -0
- package/src/hub/memory-observations.ts +47 -0
- package/src/hub/memory-plan-guard.ts +33 -0
- package/src/hub/memory-policy.ts +42 -0
- package/src/hub/memory-profile-check.ts +170 -0
- package/src/hub/memory-profile.ts +192 -0
- package/src/hub/memory-staleness.ts +55 -0
- package/src/hub/path-policy.ts +8 -0
- package/src/hub/peer-catalog.ts +3 -0
- package/src/hub/project.ts +4 -37
- package/src/hub/read-tools.ts +127 -0
- package/src/hub/routing.ts +35 -16
- package/src/hub/settings.ts +2 -1
- package/src/hub/spec-read.ts +120 -0
- package/src/hub/supervision.ts +21 -6
- package/src/hub/task-sweep.ts +7 -6
- package/src/hub/tasks.ts +370 -71
- package/src/local/deny.ts +4 -17
- package/src/memory/brief.ts +8 -5
- package/src/memory/capture.ts +3 -1
- package/src/memory/recall.ts +50 -110
- package/src/pi/process-signature.ts +2 -42
- package/src/ui/index.html +12 -9
- package/src/ui/needs.ts +7 -5
- package/src/ui/office-model.ts +4 -1
- package/src/ui/task-board.ts +1 -1
- package/src/ui/task-progress.ts +6 -2
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
- package/src/memory/import.ts +0 -90
- package/src/memory/store.ts +0 -805
- package/src/memory/sync.ts +0 -145
package/src/hub/bus.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { appendNote, DIGEST, HUB, keepNotes, MAX_HOP, newEnvelope, noteLine, parseMarker, quoteNotes, replyAudience, type Envelope, type EnvelopeOpts, type PeerId, type PeerState, type Priority } from "./envelope.ts";
|
|
1
|
+
import { appendNote, DIGEST, HUB, keepNotes, MAX_HOP, mergePrefaces, newEnvelope, noteLine, parseMarker, quoteNotes, replyAudience, uniqueMemoryLines, type Envelope, type EnvelopeOpts, type PeerId, type PeerState, type Priority } from "./envelope.ts";
|
|
2
2
|
import type { PeerAdapter, DeliveryReceipt } from "./peers.ts";
|
|
3
|
-
import { DeliveryJournal, type JournalDelivery, type JournalDeliveryState } from "./delivery-journal.ts";
|
|
3
|
+
import { DeliveryJournal, MAX_ENVELOPE_BODY, type JournalDelivery, type JournalDeliveryState } from "./delivery-journal.ts";
|
|
4
|
+
import { DELIVERY_RETRY_WARNING } from "./delivery-hints.ts";
|
|
5
|
+
import { PreEffectToolRefusal } from "./tool-refusal.ts";
|
|
4
6
|
|
|
5
7
|
export type { DeliveryReceipt } from "./peers.ts";
|
|
6
8
|
|
|
@@ -135,6 +137,8 @@ export class Bus {
|
|
|
135
137
|
onDelivered?: (peer: PeerId, originals: Envelope[]) => void;
|
|
136
138
|
/** Before deliver's synchronous busy transition, with originals rather than a condensed digest. */
|
|
137
139
|
onDeliver?: (peer: PeerId, originals: Envelope[]) => void;
|
|
140
|
+
/** Final rendered journal payload, and its first positive acknowledgement. Observation cannot alter settlement. */
|
|
141
|
+
onMemoryDelivery?: (peer: PeerId, deliveryId: string, out: Envelope[], field: "rendered" | "delivered") => void;
|
|
138
142
|
/** Synchronous native failure observation, before queue callbacks can finish turn metrics. */
|
|
139
143
|
onDeliveryFailed?: (peer: PeerId) => void;
|
|
140
144
|
private recoveryHeld = false;
|
|
@@ -246,10 +250,11 @@ export class Bus {
|
|
|
246
250
|
catch { this.storageError = "delivery journal unavailable"; throw new Error("delivery journal unavailable"); }
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
private durableHandoff(peer: PeerId, deliveryId: string, originals: Envelope[], out: Envelope[]): void {
|
|
253
|
+
private durableHandoff(peer: PeerId, deliveryId: string, originals: Envelope[], out: Envelope[], observe = true): void {
|
|
250
254
|
if (!this.journal) return;
|
|
251
255
|
const delivery = { id: deliveryId, peer, state: "dispatching" as const, createdAt: Date.now(), originals, out };
|
|
252
256
|
this.journal.checkpointDelivery(this.snapshotWithoutJournal(), delivery);
|
|
257
|
+
if (observe) this.memoryDelivery(peer, deliveryId, out, "rendered", originals);
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
private snapshotWithoutJournal(): BusSnapshot {
|
|
@@ -259,6 +264,7 @@ export class Bus {
|
|
|
259
264
|
/** Stop new deliveries while a coordinator takes a stable snapshot. Existing adapter turns are left alone. */
|
|
260
265
|
setRecoveryHold(held: boolean): void {
|
|
261
266
|
this.recoveryHeld = held;
|
|
267
|
+
if (!held) this.onQueues?.(); // restored holds need classification even when no peer is attached
|
|
262
268
|
if (!held) for (const id of this.peers.keys()) void this.drain(id);
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -327,7 +333,7 @@ export class Bus {
|
|
|
327
333
|
this.queues.clear();
|
|
328
334
|
for (const [id, queue] of Object.entries(snapshot.queues ?? {})) this.queues.set(id, queue.map((e) => ({ ...e, ...(e.to ? { to: [...e.to] } : {}) })));
|
|
329
335
|
this.prefaces.clear();
|
|
330
|
-
for (const [id, e] of Object.entries(snapshot.prefaces ?? {})) this.prefaces.set(id, { ...e, ...(e.to ? { to: [...e.to] } : {}) });
|
|
336
|
+
for (const [id, e] of Object.entries(snapshot.prefaces ?? {})) this.prefaces.set(id, { ...e, body: uniqueMemoryLines(e.body), ...(e.to ? { to: [...e.to] } : {}) });
|
|
331
337
|
this.seen.clear();
|
|
332
338
|
for (const e of snapshot.seen ?? []) this.seen.set(e.id, { ...e, ...(e.to ? { to: [...e.to] } : {}) });
|
|
333
339
|
this.attempts.clear();
|
|
@@ -357,16 +363,41 @@ export class Bus {
|
|
|
357
363
|
|
|
358
364
|
queueShow(id: string): JournalDelivery | undefined { return this.journal?.get(id) ?? this.queueList().find((d) => d.id === id); }
|
|
359
365
|
|
|
360
|
-
|
|
366
|
+
heldDeliveryHints(peer: string) { return this.storageError ? undefined : this.journal?.heldDeliveryHints(peer); }
|
|
367
|
+
|
|
368
|
+
/** Consent is for this peer's held row only, never authority to complete/discard or release a pause. */
|
|
369
|
+
retryDelivery(peer: string, id: string): void {
|
|
370
|
+
const refuse = () => { throw new PreEffectToolRefusal("recipient retry unavailable; inspect the held delivery or ask the person"); };
|
|
371
|
+
if (this.closed || this.recoveryHeld || this.storageError || peer === "user" || peer === HUB) return refuse();
|
|
372
|
+
const row = this.queueShow(id);
|
|
373
|
+
if (!row || row.peer !== peer || row.state !== "needs_review" || !this.journal || this.journal.retryEvidence(peer, row.originals) !== "unused") return refuse();
|
|
374
|
+
this.resolveDelivery(id, row.revision, "retry", "recipient requested one uncertain redelivery", `peer:${peer}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private retryOut(peer: string, originals: Envelope[], out: Envelope[]): Envelope[] {
|
|
378
|
+
if (!out.length || !this.journal || !this.journal.retryWarning(peer, originals)) return out;
|
|
379
|
+
return out.map((env, index) => {
|
|
380
|
+
if (index !== 0) return env;
|
|
381
|
+
const prefix = `${DELIVERY_RETRY_WARNING}\n\n`, marker = "\n[cut: rendered body exceeds delivery limit]";
|
|
382
|
+
if (prefix.length + env.body.length <= MAX_ENVELOPE_BODY) return { ...env, body: prefix + env.body };
|
|
383
|
+
let end = MAX_ENVELOPE_BODY - prefix.length - marker.length;
|
|
384
|
+
// The journal cap uses UTF-16 length; never cut between a Unicode surrogate pair.
|
|
385
|
+
if (Array.from(env.body.slice(end - 1, end + 1))[0]?.length === 2) end--;
|
|
386
|
+
return { ...env, body: prefix + env.body.slice(0, end) + marker };
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
resolveDelivery(id: string, revision: number, action: "completed" | "retry" | "discard", reason: string, actor: string = "user"): void {
|
|
361
391
|
if (!this.journal || this.storageError) throw new Error("delivery journal unavailable");
|
|
362
392
|
const record = this.queueShow(id);
|
|
363
393
|
if (!record) throw new Error("unknown delivery");
|
|
364
394
|
const previous = this.journal.resolution(id);
|
|
365
395
|
if (previous) {
|
|
366
|
-
this.journal.resolve(id, revision, action, reason); // verifies identical request; no second enqueue
|
|
396
|
+
this.journal.resolve(id, revision, action, reason, revision, actor); // verifies identical request; no second enqueue
|
|
367
397
|
return;
|
|
368
398
|
}
|
|
369
|
-
|
|
399
|
+
const recipientConsent = actor === `peer:${record.peer}` && action === "retry" && record.state === "needs_review";
|
|
400
|
+
if (["dispatching", "accepted"].includes(record.state) || (this.peers.get(record.peer)?.state === "busy" && !recipientConsent)) throw new Error("active delivery cannot be resolved");
|
|
370
401
|
if (record.revision !== revision) throw new Error("stale delivery revision");
|
|
371
402
|
if (record.state === "queued" && action === "retry") throw new Error("queued delivery is already scheduled");
|
|
372
403
|
if (action === "retry" && (this.queues.get(record.peer)?.length ?? 0) + record.originals.length > this.opts.queueCap) throw new Error("recipient queue is full");
|
|
@@ -378,17 +409,17 @@ export class Bus {
|
|
|
378
409
|
this.journal.transaction(() => {
|
|
379
410
|
if (!this.journal!.get(id)) this.journal!.createDelivery({ ...record });
|
|
380
411
|
const current = this.journal!.get(id)!;
|
|
381
|
-
this.journal!.resolve(id, current.revision, action, reason, revision);
|
|
412
|
+
this.journal!.resolve(id, current.revision, action, reason, revision, actor);
|
|
382
413
|
if (record.state === "queued") {
|
|
383
414
|
const ids = new Set(record.originals.map((env) => env.id));
|
|
384
415
|
queue.splice(0, queue.length, ...queue.filter((env) => !ids.has(env.id)));
|
|
385
416
|
}
|
|
386
|
-
if (action === "retry") {
|
|
417
|
+
if (action === "retry" || action === "completed" && actor === "hub") {
|
|
387
418
|
for (const env of record.originals) {
|
|
388
419
|
if (env.from === HUB && env.kind === "presence") this.restorePreface(record.peer, env);
|
|
389
|
-
else if (!queue.some((old) => old.id === env.id)) queue.push(env);
|
|
420
|
+
else if (action === "retry" && !queue.some((old) => old.id === env.id)) queue.push(env);
|
|
390
421
|
}
|
|
391
|
-
this.queues.set(record.peer, queue);
|
|
422
|
+
if (action === "retry") this.queues.set(record.peer, queue);
|
|
392
423
|
}
|
|
393
424
|
this.persist();
|
|
394
425
|
});
|
|
@@ -433,8 +464,12 @@ export class Bus {
|
|
|
433
464
|
const before = this.journal.get(id);
|
|
434
465
|
const state: JournalDeliveryState = receipt.state === "accepted" ? "accepted" : receipt.state === "completed" ? "completed" : receipt.state === "failed_safe" ? "failed" : "needs_review";
|
|
435
466
|
if (state === "failed" || state === "needs_review") this.deliveryFailed(peer);
|
|
436
|
-
this.journal.
|
|
437
|
-
|
|
467
|
+
if (state === "completed") this.journal.completeDelivered(id, peer, "adapter", receipt.reason);
|
|
468
|
+
else this.journal.transition(id, state, receipt.reason);
|
|
469
|
+
if (before?.state === "dispatching" && (state === "accepted" || state === "completed")) {
|
|
470
|
+
this.memoryDelivery(peer, id, before.out, "delivered", before.originals);
|
|
471
|
+
this.delivered(peer, before.originals);
|
|
472
|
+
}
|
|
438
473
|
if (state === "failed") {
|
|
439
474
|
if (before) this.failed(peer, before.originals, before.originals.some((e) => e.private) ? "private delivery failed" : receipt.reason, receipt.quota === true && before.state === "dispatching");
|
|
440
475
|
}
|
|
@@ -457,8 +492,9 @@ export class Bus {
|
|
|
457
492
|
for (const [id, owner] of this.activeDeliveries) {
|
|
458
493
|
if (owner !== peer) continue;
|
|
459
494
|
const row = this.journal.get(id);
|
|
460
|
-
if (row?.out.some((e) => e.id === envelopeId)) {
|
|
461
|
-
this.journal.
|
|
495
|
+
if (row?.peer === peer && ["dispatching", "accepted"].includes(row.state) && row.out.some((e) => e.id === envelopeId)) {
|
|
496
|
+
this.journal.completeDelivered(id, peer, "reply", "correlated reply");
|
|
497
|
+
if (row.state === "dispatching") this.memoryDelivery(peer, id, row.out, "delivered", row.originals);
|
|
462
498
|
this.activeDeliveries.delete(id);
|
|
463
499
|
this.refreshRecoveryHold(peer);
|
|
464
500
|
this.onQueues?.();
|
|
@@ -529,12 +565,12 @@ export class Bus {
|
|
|
529
565
|
|
|
530
566
|
/** Context the hub wants the peer to see once: it rides in front of the next delivery instead of costing a turn of its own. */
|
|
531
567
|
preface(id: PeerId, text: string): void {
|
|
532
|
-
const body = quoteNotes(text);
|
|
568
|
+
const body = uniqueMemoryLines(quoteNotes(text));
|
|
533
569
|
const existing = this.prefaces.get(id);
|
|
534
570
|
if (existing) {
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
this.prefaces.set(id, { ...existing, body:
|
|
571
|
+
// A fresh daemon recalls again after restoring queued context. Merge repeated blocks,
|
|
572
|
+
// retain distinct context and keep the original envelope id/hop for retry bookkeeping.
|
|
573
|
+
this.prefaces.set(id, { ...existing, body: mergePrefaces(existing.body, body) });
|
|
538
574
|
this.persist();
|
|
539
575
|
return;
|
|
540
576
|
}
|
|
@@ -545,14 +581,14 @@ export class Bus {
|
|
|
545
581
|
/** Another peer's shared note: rides in the preface like recall. The newest NOTES_KEPT stay; older ones remain in claude-mem. */
|
|
546
582
|
note(id: PeerId, line: string): void {
|
|
547
583
|
const existing = this.prefaces.get(id);
|
|
548
|
-
this.prefaces.set(id, existing ? { ...existing, body: keepNotes(appendNote(existing.body, line), NOTES_KEPT) } : newEnvelope(HUB, line, { to: [id], kind: "presence" }));
|
|
584
|
+
this.prefaces.set(id, existing ? { ...existing, body: keepNotes(uniqueMemoryLines(appendNote(existing.body, line)), NOTES_KEPT) } : newEnvelope(HUB, uniqueMemoryLines(line), { to: [id], kind: "presence" }));
|
|
549
585
|
this.persist();
|
|
550
586
|
}
|
|
551
587
|
|
|
552
588
|
/** A preface that went out with a failed delivery comes back ahead of whatever was prefaced meanwhile, instead of replacing it. */
|
|
553
589
|
private restorePreface(id: PeerId, env: Envelope): void {
|
|
554
590
|
const now = this.prefaces.get(id);
|
|
555
|
-
this.prefaces.set(id, !now ? env : now.id === env.id ? now : { ...env, body: keepNotes(
|
|
591
|
+
this.prefaces.set(id, !now ? { ...env, body: uniqueMemoryLines(env.body) } : now.id === env.id ? { ...now, body: uniqueMemoryLines(now.body) } : { ...env, body: keepNotes(mergePrefaces(env.body, now.body), NOTES_KEPT) });
|
|
556
592
|
}
|
|
557
593
|
|
|
558
594
|
/** Returns the peers the envelope was queued for. */
|
|
@@ -637,6 +673,11 @@ export class Bus {
|
|
|
637
673
|
return this.seen.get(id);
|
|
638
674
|
}
|
|
639
675
|
|
|
676
|
+
/** The envelopes most recently handed to a peer, used to distinguish a received request from an unread queue. */
|
|
677
|
+
lastDeliveredTo(peer: PeerId): Envelope[] {
|
|
678
|
+
return this.lastDelivery.get(peer)?.out ?? [];
|
|
679
|
+
}
|
|
680
|
+
|
|
640
681
|
/**
|
|
641
682
|
* Take back an envelope that has not been delivered yet (a request that has expired). True when it was removed from a
|
|
642
683
|
* queue now; an envelope whose steer is still in flight is not in any queue, so its id is remembered and it is
|
|
@@ -684,6 +725,11 @@ export class Bus {
|
|
|
684
725
|
this.emit({ t: "stale", env, peer, reason });
|
|
685
726
|
}
|
|
686
727
|
|
|
728
|
+
private memoryDelivery(peer: PeerId, id: string, out: Envelope[], field: "rendered" | "delivered", originals: Envelope[] = out): void {
|
|
729
|
+
if ([...out, ...originals].some(env => env.private)) return;
|
|
730
|
+
try { this.onMemoryDelivery?.(peer, id, out, field); } catch { /* optional telemetry never alters delivery */ }
|
|
731
|
+
}
|
|
732
|
+
|
|
687
733
|
private delivered(peer: PeerId, originals: Envelope[]): void {
|
|
688
734
|
try { this.onDelivered?.(peer, originals); } catch { /* observation must never affect delivery settlement */ }
|
|
689
735
|
}
|
|
@@ -766,14 +812,20 @@ export class Bus {
|
|
|
766
812
|
if (index < 0) return;
|
|
767
813
|
this.steering++;
|
|
768
814
|
const deliveryId = crypto.randomUUID();
|
|
815
|
+
let removed = false, handedOff = false;
|
|
769
816
|
try {
|
|
817
|
+
const out = this.retryOut(id, [env], [env]);
|
|
770
818
|
queue.splice(index, 1);
|
|
771
|
-
|
|
819
|
+
removed = true;
|
|
820
|
+
this.durableHandoff(id, deliveryId, [env], out);
|
|
821
|
+
handedOff = true;
|
|
772
822
|
this.activeDeliveries.set(deliveryId, id);
|
|
773
|
-
try { await peer.steer(
|
|
823
|
+
try { await peer.steer(out, deliveryId); }
|
|
774
824
|
catch { this.uncertain(id, deliveryId, "steering outcome is uncertain"); }
|
|
775
|
-
} catch {
|
|
776
|
-
|
|
825
|
+
} catch {
|
|
826
|
+
if (removed && !handedOff) queue.splice(index, 0, env);
|
|
827
|
+
this.storageError = "delivery journal unavailable";
|
|
828
|
+
} finally { this.steering--; this.onQueues?.(); }
|
|
777
829
|
}
|
|
778
830
|
|
|
779
831
|
/** Holds on a peer's queue, for a drain and a pull alike: off roster, recovery, an uncertain delivery, a pause (console, budget, conductor). */
|
|
@@ -812,14 +864,16 @@ export class Bus {
|
|
|
812
864
|
if (!preface && !queue.length) return [];
|
|
813
865
|
const queued = [...queue];
|
|
814
866
|
const batch = [...(preface ? [preface] : []), ...this.take(id, queue)];
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.remember(batch);
|
|
867
|
+
let out: Envelope[] = [];
|
|
868
|
+
const deliveryId = crypto.randomUUID();
|
|
818
869
|
try {
|
|
819
|
-
|
|
870
|
+
out = this.retryOut(id, batch, batch);
|
|
871
|
+
this.prefaces.delete(id);
|
|
872
|
+
// Before the checkpoint, as in a drain, so the persisted snapshot carries it; a failed policy read rolls back too.
|
|
873
|
+
this.remember(out);
|
|
820
874
|
this.journal?.transaction(() => {
|
|
821
|
-
this.durableHandoff(id, deliveryId, batch,
|
|
822
|
-
this.journal!.
|
|
875
|
+
this.durableHandoff(id, deliveryId, batch, out, false); // observe only after this outer transaction commits
|
|
876
|
+
this.journal!.completeDelivered(deliveryId, id, "pull", "read through hub_inbox");
|
|
823
877
|
});
|
|
824
878
|
} catch (error) {
|
|
825
879
|
queue.splice(0, queue.length, ...queued);
|
|
@@ -827,9 +881,13 @@ export class Bus {
|
|
|
827
881
|
this.storageError = "delivery journal unavailable";
|
|
828
882
|
throw error;
|
|
829
883
|
}
|
|
884
|
+
if (this.journal) {
|
|
885
|
+
this.memoryDelivery(id, deliveryId, out, "rendered", batch);
|
|
886
|
+
this.memoryDelivery(id, deliveryId, out, "delivered", batch);
|
|
887
|
+
}
|
|
830
888
|
this.failureStreak.delete(id); this.lastFailure.delete(id); // read, as a completed push receipt clears them
|
|
831
889
|
this.onQueues?.();
|
|
832
|
-
return
|
|
890
|
+
return out;
|
|
833
891
|
}
|
|
834
892
|
|
|
835
893
|
private async drain(id: PeerId): Promise<void> {
|
|
@@ -851,8 +909,11 @@ export class Bus {
|
|
|
851
909
|
const delivery = preface ? [preface, ...batch] : batch;
|
|
852
910
|
const mayCondense = this.opts.condense && !delivery.some((e) => e.priority === "important");
|
|
853
911
|
this.condensing += mayCondense ? 1 : 0;
|
|
854
|
-
|
|
855
|
-
|
|
912
|
+
let out: Envelope[];
|
|
913
|
+
try {
|
|
914
|
+
const condensed = mayCondense ? await this.opts.condense!(delivery).catch(() => delivery) : delivery;
|
|
915
|
+
out = this.retryOut(id, delivery, condensed);
|
|
916
|
+
} finally { this.condensing -= mayCondense ? 1 : 0; }
|
|
856
917
|
// A session that cannot show pushes may have taken the peer while this was condensed (issue #205).
|
|
857
918
|
if (!this.deliverable(id)) {
|
|
858
919
|
if (!this.journal) { if (preface) this.restorePreface(id, preface); queue.unshift(...batch.filter((e) => !this.withdrawn.has(e.id))); }
|
|
@@ -864,16 +925,25 @@ export class Bus {
|
|
|
864
925
|
this.dropIrrelevant(id, queue);
|
|
865
926
|
continue;
|
|
866
927
|
}
|
|
928
|
+
const deliveryId = crypto.randomUUID();
|
|
867
929
|
if (this.journal) {
|
|
868
930
|
if (this.prefaces.get(id) !== preface || batch.some((e) => !queue.some((item) => item.id === e.id))) continue;
|
|
931
|
+
const queued = [...queue], seen = [...this.seen];
|
|
869
932
|
for (const env of batch) queue.splice(queue.findIndex((item) => item.id === env.id), 1);
|
|
870
933
|
this.prefaces.delete(id);
|
|
871
|
-
|
|
872
|
-
|
|
934
|
+
this.remember(out); // the checkpoint must retain rendered reply ids across a restart
|
|
935
|
+
try { this.durableHandoff(id, deliveryId, delivery, out); }
|
|
936
|
+
catch (error) {
|
|
937
|
+
queue.splice(0, queue.length, ...queued);
|
|
938
|
+
if (preface) this.prefaces.set(id, preface);
|
|
939
|
+
this.seen.clear();
|
|
940
|
+
for (const [key, env] of seen) this.seen.set(key, env);
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
this.activeDeliveries.set(deliveryId, id);
|
|
944
|
+
} else this.remember(out);
|
|
873
945
|
this.lastDelivery.set(id, { out, originals: delivery });
|
|
874
946
|
this.preparing.delete(id); // from here on the transport/journal owns settlement
|
|
875
|
-
const deliveryId = crypto.randomUUID();
|
|
876
|
-
if (this.journal) { this.durableHandoff(id, deliveryId, delivery, out); this.activeDeliveries.set(deliveryId, id); }
|
|
877
947
|
try {
|
|
878
948
|
try { this.onDeliver?.(id, delivery); } catch { /* observation must never affect delivery */ }
|
|
879
949
|
if (this.journal) await peer.deliver(out, deliveryId);
|