fix(channels): bound thread binding lifecycle durations

This commit is contained in:
Peter Steinberger
2026-05-30 18:01:36 -04:00
parent 5e139e32dc
commit 11b5968534
2 changed files with 36 additions and 9 deletions

View File

@@ -1,8 +1,11 @@
import { beforeEach, describe, expect, it } from "vitest";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { MAX_DATE_TIMESTAMP_MS } from "../shared/number-coercion.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import {
requiresNativeThreadContextForThreadHere,
resolveThreadBindingIdleTimeoutMs,
resolveThreadBindingMaxAgeMs,
resolveThreadBindingPlacementForCurrentContext,
resolveThreadBindingSpawnPolicy,
supportsAutomaticThreadBindingSpawn,
@@ -80,6 +83,21 @@ describe("thread binding spawn policy helpers", () => {
expect(policy.defaultSpawnContext).toBe("fork");
});
it("preserves long lifecycle hour values while capping unsafe conversions", () => {
expect(
resolveThreadBindingIdleTimeoutMs({
channelIdleHoursRaw: 720,
sessionIdleHoursRaw: undefined,
}),
).toBe(2_592_000_000);
expect(
resolveThreadBindingMaxAgeMs({
channelMaxAgeHoursRaw: undefined,
sessionMaxAgeHoursRaw: Number.MAX_SAFE_INTEGER,
}),
).toBe(MAX_DATE_TIMESTAMP_MS);
});
it("uses spawnSessions for both subagent and ACP spawn policy", () => {
const cfg = {
channels: {

View File

@@ -1,3 +1,4 @@
import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeAccountId } from "../routing/session-key.js";
@@ -94,26 +95,34 @@ function normalizeThreadBindingHours(raw: unknown): number | undefined {
return raw;
}
function resolveThreadBindingHoursMs(raw: unknown, fallbackHours: number): number {
const hours = normalizeThreadBindingHours(raw) ?? fallbackHours;
const durationMs = Math.floor(hours * 60 * 60 * 1000);
if (!Number.isFinite(durationMs) || durationMs < 0) {
return 0;
}
return Math.min(durationMs, MAX_DATE_TIMESTAMP_MS);
}
export function resolveThreadBindingIdleTimeoutMs(params: {
channelIdleHoursRaw: unknown;
sessionIdleHoursRaw: unknown;
}): number {
const idleHours =
normalizeThreadBindingHours(params.channelIdleHoursRaw) ??
normalizeThreadBindingHours(params.sessionIdleHoursRaw) ??
DEFAULT_THREAD_BINDING_IDLE_HOURS;
return Math.floor(idleHours * 60 * 60 * 1000);
return resolveThreadBindingHoursMs(
params.channelIdleHoursRaw,
normalizeThreadBindingHours(params.sessionIdleHoursRaw) ?? DEFAULT_THREAD_BINDING_IDLE_HOURS,
);
}
export function resolveThreadBindingMaxAgeMs(params: {
channelMaxAgeHoursRaw: unknown;
sessionMaxAgeHoursRaw: unknown;
}): number {
const maxAgeHours =
normalizeThreadBindingHours(params.channelMaxAgeHoursRaw) ??
return resolveThreadBindingHoursMs(
params.channelMaxAgeHoursRaw,
normalizeThreadBindingHours(params.sessionMaxAgeHoursRaw) ??
DEFAULT_THREAD_BINDING_MAX_AGE_HOURS;
return Math.floor(maxAgeHours * 60 * 60 * 1000);
DEFAULT_THREAD_BINDING_MAX_AGE_HOURS,
);
}
export function resolveThreadBindingEffectiveExpiresAt(params: {