Skip to content

Commit 0e3687c

Browse files
anagnorisis2peripeteiashakkernerd
authored andcommitted
fix(heartbeat): deliver origin-carrying events when no heartbeat target is configured
The wake path carries the originating session's delivery context onto the system event, but resolveHeartbeatDeliveryTarget defaulted to target "none" whenever agents.defaults.heartbeat was absent — the woken turn ran and its reply was silently dropped. When no heartbeat target is configured and the drained event explicitly carried a deliverable origin turn source, resolve delivery to that origin; an explicit target "none" still suppresses, and runs without an origin turn source keep the previous default.
1 parent f5705a9 commit 0e3687c

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

src/infra/outbound/targets.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,55 @@ describe("resolveSessionDeliveryTarget", () => {
493493
expect(resolved.to).toBe("room:ops:topic:1008013");
494494
});
495495

496+
it("delivers an origin-carrying event when no heartbeat target is configured", () => {
497+
// A wake/cron event that explicitly carried its origin delivery context
498+
// names its own destination; the reply must not be dropped just because
499+
// the deployment never configured agents.defaults.heartbeat.
500+
const resolved = resolveHeartbeatDeliveryTarget({
501+
cfg: {},
502+
entry: {
503+
sessionId: "sess-origin-no-config",
504+
updatedAt: 1,
505+
lastChannel: "alpha",
506+
lastTo: "chat:one",
507+
},
508+
turnSource: { channel: "alpha", to: "chat:one", threadId: "77" },
509+
});
510+
expect(resolved.channel).toBe("alpha");
511+
expect(resolved.to).toBe("chat:one");
512+
expect(resolved.threadId).toBe("77");
513+
});
514+
515+
it("keeps an explicit target:none suppressing origin-carrying events", () => {
516+
const resolved = resolveHeartbeatDeliveryTarget({
517+
cfg: {},
518+
entry: {
519+
sessionId: "sess-origin-target-none",
520+
updatedAt: 1,
521+
lastChannel: "alpha",
522+
lastTo: "chat:one",
523+
},
524+
heartbeat: { target: "none" },
525+
turnSource: { channel: "alpha", to: "chat:one" },
526+
});
527+
expect(resolved.channel).toBe("none");
528+
expect(resolved.reason).toBe("target-none");
529+
});
530+
531+
it("stays suppressed with unset heartbeat config and no origin turn source", () => {
532+
const resolved = resolveHeartbeatDeliveryTarget({
533+
cfg: {},
534+
entry: {
535+
sessionId: "sess-no-config-no-origin",
536+
updatedAt: 1,
537+
lastChannel: "alpha",
538+
lastTo: "chat:one",
539+
},
540+
});
541+
expect(resolved.channel).toBe("none");
542+
expect(resolved.reason).toBe("target-none");
543+
});
544+
496545
const resolveHeartbeatTarget = (entry: SessionEntry, directPolicy?: "allow" | "block") =>
497546
resolveHeartbeatDeliveryTarget({
498547
cfg: {},

src/infra/outbound/targets.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ export function resolveHeartbeatDeliveryTarget(params: {
111111
if (normalized) {
112112
target = normalized;
113113
}
114+
} else if (
115+
rawTarget === undefined &&
116+
params.turnSource?.to &&
117+
params.turnSource.channel &&
118+
isDeliverableMessageChannel(params.turnSource.channel)
119+
) {
120+
// No heartbeat target configured, but this run drains an event that
121+
// explicitly carried its origin delivery context (e.g. a cron wake from a
122+
// channel thread/topic). The event named its destination, so deliver to it
123+
// instead of silently dropping the reply. An explicit `target: "none"`
124+
// still suppresses delivery (operator opt-out above takes precedence).
125+
target = "last";
114126
}
115127

116128
if (target === "none") {

0 commit comments

Comments
 (0)