From a3b047b5fcfbc59391c6d4b9370b1ca65d36d20a Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:29:15 -0500 Subject: [PATCH] Preserve Discord lifecycle windows on rebind --- .../monitor/thread-bindings.lifecycle.test.ts | 56 +++++++++++++++++++ .../src/monitor/thread-bindings.manager.ts | 5 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts index 843291571d6..60e05db5d48 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -458,6 +458,62 @@ describe("thread binding lifecycle", () => { } }); + it("preserves explicit lifecycle windows when rebinding the same thread", async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date("2026-02-20T10:00:00.000Z")); + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + await manager.bindTarget({ + threadId: "thread-1", + channelId: "parent-1", + targetKind: "subagent", + targetSessionKey: "agent:main:subagent:child", + agentId: "main", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + setThreadBindingIdleTimeoutBySessionKey({ + accountId: "default", + targetSessionKey: "agent:main:subagent:child", + idleTimeoutMs: 2 * 60 * 60 * 1000, + }); + setThreadBindingMaxAgeBySessionKey({ + accountId: "default", + targetSessionKey: "agent:main:subagent:child", + maxAgeMs: 3 * 60 * 60 * 1000, + }); + + vi.setSystemTime(new Date("2026-02-20T10:30:00.000Z")); + const rebound = await manager.bindTarget({ + threadId: "thread-1", + channelId: "parent-1", + targetKind: "subagent", + targetSessionKey: "agent:main:subagent:child", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + expect(rebound).toMatchObject({ + idleTimeoutMs: 2 * 60 * 60 * 1000, + maxAgeMs: 3 * 60 * 60 * 1000, + }); + expect(requireBinding(manager, "thread-1")).toMatchObject({ + idleTimeoutMs: 2 * 60 * 60 * 1000, + maxAgeMs: 3 * 60 * 60 * 1000, + }); + } finally { + vi.useRealTimers(); + } + }); + it("keeps binding when idle timeout is disabled per session key", async () => { vi.useFakeTimers(); try { diff --git a/extensions/discord/src/monitor/thread-bindings.manager.ts b/extensions/discord/src/monitor/thread-bindings.manager.ts index c8155f86a0f..a2cc0d1db21 100644 --- a/extensions/discord/src/monitor/thread-bindings.manager.ts +++ b/extensions/discord/src/monitor/thread-bindings.manager.ts @@ -474,8 +474,9 @@ export function createThreadBindingManager( "system", boundAt: now, lastActivityAt: now, - idleTimeoutMs, - maxAgeMs, + idleTimeoutMs: + typeof existing?.idleTimeoutMs === "number" ? existing.idleTimeoutMs : idleTimeoutMs, + maxAgeMs: typeof existing?.maxAgeMs === "number" ? existing.maxAgeMs : maxAgeMs, metadata: bindParams.metadata && typeof bindParams.metadata === "object" ? { ...existing?.metadata, ...bindParams.metadata }