mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 11:30:41 +00:00
refactor: remove remaining thread-binding ttl legacy paths
This commit is contained in:
@@ -638,7 +638,7 @@ Add independent ACP dispatch kill switch:
|
||||
|
||||
- `/focus <sessionKey>` continues to support ACP targets
|
||||
- `/unfocus` keeps current semantics
|
||||
- `/session ttl` remains the top level TTL override
|
||||
- `/session idle` and `/session max-age` replace the old TTL override
|
||||
|
||||
## Phased rollout
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ When thread bindings are enabled for a channel adapter, ACP sessions can be boun
|
||||
- OpenClaw binds a thread to a target ACP session.
|
||||
- Follow-up messages in that thread route to the bound ACP session.
|
||||
- ACP output is delivered back to the same thread.
|
||||
- Unfocus/close/archive/TTL expiry removes the binding.
|
||||
- Unfocus/close/archive/idle-timeout or max-age expiry removes the binding.
|
||||
|
||||
Thread binding support is adapter-specific. If the active channel adapter does not support thread bindings, OpenClaw returns a clear unsupported/unavailable message.
|
||||
|
||||
|
||||
@@ -746,104 +746,6 @@ describe("thread binding lifecycle", () => {
|
||||
expect(manager.getByThreadId("thread-acp-uncertain")).toBeDefined();
|
||||
});
|
||||
|
||||
it("migrates legacy expiresAt bindings to idle/max-age semantics", () => {
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thread-bindings-"));
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
try {
|
||||
__testing.resetThreadBindingsForTests();
|
||||
const bindingsPath = __testing.resolveThreadBindingsPath();
|
||||
fs.mkdirSync(path.dirname(bindingsPath), { recursive: true });
|
||||
const boundAt = Date.now() - 10_000;
|
||||
const expiresAt = boundAt + 60_000;
|
||||
fs.writeFileSync(
|
||||
bindingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
bindings: {
|
||||
"thread-legacy-active": {
|
||||
accountId: "default",
|
||||
channelId: "parent-1",
|
||||
threadId: "thread-legacy-active",
|
||||
targetKind: "subagent",
|
||||
targetSessionKey: "agent:main:subagent:legacy-active",
|
||||
agentId: "main",
|
||||
boundBy: "system",
|
||||
boundAt,
|
||||
expiresAt,
|
||||
},
|
||||
"thread-legacy-disabled": {
|
||||
accountId: "default",
|
||||
channelId: "parent-1",
|
||||
threadId: "thread-legacy-disabled",
|
||||
targetKind: "subagent",
|
||||
targetSessionKey: "agent:main:subagent:legacy-disabled",
|
||||
agentId: "main",
|
||||
boundBy: "system",
|
||||
boundAt,
|
||||
expiresAt: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const manager = createThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
||||
maxAgeMs: 0,
|
||||
});
|
||||
|
||||
const active = manager.getByThreadId("thread-legacy-active");
|
||||
expect(active).toBeDefined();
|
||||
expect(active?.idleTimeoutMs).toBe(0);
|
||||
expect(active?.maxAgeMs).toBe(expiresAt - boundAt);
|
||||
expect(
|
||||
resolveThreadBindingMaxAgeExpiresAt({
|
||||
record: active!,
|
||||
defaultMaxAgeMs: manager.getMaxAgeMs(),
|
||||
}),
|
||||
).toBe(expiresAt);
|
||||
expect(
|
||||
resolveThreadBindingInactivityExpiresAt({
|
||||
record: active!,
|
||||
defaultIdleTimeoutMs: manager.getIdleTimeoutMs(),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
const disabled = manager.getByThreadId("thread-legacy-disabled");
|
||||
expect(disabled).toBeDefined();
|
||||
expect(disabled?.idleTimeoutMs).toBe(0);
|
||||
expect(disabled?.maxAgeMs).toBe(0);
|
||||
expect(
|
||||
resolveThreadBindingMaxAgeExpiresAt({
|
||||
record: disabled!,
|
||||
defaultMaxAgeMs: manager.getMaxAgeMs(),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveThreadBindingInactivityExpiresAt({
|
||||
record: disabled!,
|
||||
defaultIdleTimeoutMs: manager.getIdleTimeoutMs(),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
__testing.resetThreadBindingsForTests();
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("persists unbinds even when no manager is active", () => {
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thread-bindings-"));
|
||||
|
||||
@@ -6,7 +6,7 @@ import { normalizeAccountId, resolveAgentIdFromSessionKey } from "../../routing/
|
||||
import {
|
||||
DEFAULT_THREAD_BINDING_IDLE_TIMEOUT_MS,
|
||||
DEFAULT_THREAD_BINDING_MAX_AGE_MS,
|
||||
RECENT_UNBOUND_WEBHOOK_ECHO_TTL_MS,
|
||||
RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS,
|
||||
THREAD_BINDINGS_VERSION,
|
||||
type PersistedThreadBindingRecord,
|
||||
type PersistedThreadBindingsPayload,
|
||||
@@ -177,29 +177,6 @@ function normalizePersistedBinding(threadIdKey: string, raw: unknown): ThreadBin
|
||||
typeof value.maxAgeMs === "number" && Number.isFinite(value.maxAgeMs)
|
||||
? Math.max(0, Math.floor(value.maxAgeMs))
|
||||
: undefined;
|
||||
const legacyExpiresAt =
|
||||
typeof (value as { expiresAt?: unknown }).expiresAt === "number" &&
|
||||
Number.isFinite((value as { expiresAt?: unknown }).expiresAt)
|
||||
? Math.max(0, Math.floor((value as { expiresAt?: number }).expiresAt ?? 0))
|
||||
: undefined;
|
||||
|
||||
let migratedIdleTimeoutMs = idleTimeoutMs;
|
||||
let migratedMaxAgeMs = maxAgeMs;
|
||||
if (
|
||||
migratedIdleTimeoutMs === undefined &&
|
||||
migratedMaxAgeMs === undefined &&
|
||||
legacyExpiresAt != null
|
||||
) {
|
||||
if (legacyExpiresAt <= 0) {
|
||||
migratedIdleTimeoutMs = 0;
|
||||
migratedMaxAgeMs = 0;
|
||||
} else {
|
||||
const baseBoundAt = boundAt > 0 ? boundAt : lastActivityAt;
|
||||
// Legacy expiresAt represented an absolute timestamp; map it to max-age and disable idle timeout.
|
||||
migratedIdleTimeoutMs = 0;
|
||||
migratedMaxAgeMs = Math.max(1, legacyExpiresAt - Math.max(0, baseBoundAt));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
@@ -214,8 +191,8 @@ function normalizePersistedBinding(threadIdKey: string, raw: unknown): ThreadBin
|
||||
boundBy,
|
||||
boundAt,
|
||||
lastActivityAt,
|
||||
idleTimeoutMs: migratedIdleTimeoutMs,
|
||||
maxAgeMs: migratedMaxAgeMs,
|
||||
idleTimeoutMs,
|
||||
maxAgeMs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -344,7 +321,7 @@ export function rememberRecentUnboundWebhookEcho(record: ThreadBindingRecord) {
|
||||
}
|
||||
RECENT_UNBOUND_WEBHOOK_ECHOES_BY_BINDING_KEY.set(bindingKey, {
|
||||
webhookId,
|
||||
expiresAt: Date.now() + RECENT_UNBOUND_WEBHOOK_ECHO_TTL_MS,
|
||||
expiresAt: Date.now() + RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -77,4 +77,4 @@ export const DEFAULT_THREAD_BINDING_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24
|
||||
export const DEFAULT_THREAD_BINDING_MAX_AGE_MS = 0; // disabled
|
||||
export const DEFAULT_FAREWELL_TEXT = "Thread unfocused. Messages here will no longer be routed.";
|
||||
export const DISCORD_UNKNOWN_CHANNEL_ERROR_CODE = 10_003;
|
||||
export const RECENT_UNBOUND_WEBHOOK_ECHO_TTL_MS = 30_000;
|
||||
export const RECENT_UNBOUND_WEBHOOK_ECHO_WINDOW_MS = 30_000;
|
||||
|
||||
Reference in New Issue
Block a user