mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(outbound): bound current conversation expiry
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
@@ -76,6 +76,7 @@ describe("generic current-conversation bindings", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
testing.resetCurrentConversationBindingsForTests({
|
||||
deletePersistedFile: true,
|
||||
});
|
||||
@@ -324,6 +325,64 @@ describe("generic current-conversation bindings", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("drops persisted bindings with invalid expiration timestamps", async () => {
|
||||
const filePath = testing.resolveBindingsFilePath();
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
bindings: [
|
||||
{
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1234,
|
||||
expiresAt: 8_640_000_000_000_001,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not bind generic current conversations when ttl expiry overflows", async () => {
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000));
|
||||
|
||||
await expect(
|
||||
bindGenericCurrentConversation({
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
ttlMs: 1,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("persists touched activity across reloads", async () => {
|
||||
const bound = await bindGenericCurrentConversation({
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
|
||||
@@ -6,6 +6,10 @@ import { resolveStateDir } from "../../config/paths.js";
|
||||
import { loadJsonFile } from "../../infra/json-file.js";
|
||||
import { saveJsonFile } from "../../plugin-sdk/json-store.js";
|
||||
import { getActivePluginChannelRegistryFromState } from "../../plugins/runtime-channel-state.js";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "../../shared/number-coercion.js";
|
||||
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
|
||||
import { normalizeConversationRef } from "./session-binding-normalization.js";
|
||||
import type {
|
||||
@@ -46,9 +50,15 @@ function resolveBindingsFilePath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
}
|
||||
|
||||
function isBindingExpired(record: SessionBindingRecord, now = Date.now()): boolean {
|
||||
return typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt)
|
||||
? record.expiresAt <= now
|
||||
: false;
|
||||
if (record.expiresAt === undefined) {
|
||||
return false;
|
||||
}
|
||||
const expiresAt = asDateTimestampMs(record.expiresAt);
|
||||
if (expiresAt === undefined) {
|
||||
return true;
|
||||
}
|
||||
const nowMs = asDateTimestampMs(now);
|
||||
return nowMs !== undefined && expiresAt <= nowMs;
|
||||
}
|
||||
|
||||
function toPersistedFile(): PersistedCurrentConversationBindingsFile {
|
||||
@@ -159,11 +169,24 @@ export async function bindGenericCurrentConversation(
|
||||
return null;
|
||||
}
|
||||
loadBindingsIntoMemory();
|
||||
const now = Date.now();
|
||||
const rawNow = Date.now();
|
||||
const now = asDateTimestampMs(rawNow);
|
||||
if (now === undefined) {
|
||||
return null;
|
||||
}
|
||||
const ttlMs =
|
||||
typeof input.ttlMs === "number" && Number.isFinite(input.ttlMs)
|
||||
? Math.max(0, Math.floor(input.ttlMs))
|
||||
: undefined;
|
||||
const expiresAt =
|
||||
ttlMs === undefined
|
||||
? undefined
|
||||
: ttlMs === 0
|
||||
? now
|
||||
: resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNow });
|
||||
if (ttlMs !== undefined && expiresAt === undefined) {
|
||||
return null;
|
||||
}
|
||||
const key = buildConversationKey(conversation);
|
||||
const existing = pruneExpiredBinding(key);
|
||||
const record: SessionBindingRecord = {
|
||||
@@ -173,7 +196,7 @@ export async function bindGenericCurrentConversation(
|
||||
conversation,
|
||||
status: "active",
|
||||
boundAt: now,
|
||||
...(ttlMs != null ? { expiresAt: now + ttlMs } : {}),
|
||||
...(expiresAt !== undefined ? { expiresAt } : {}),
|
||||
metadata: {
|
||||
...existing?.metadata,
|
||||
...input.metadata,
|
||||
|
||||
Reference in New Issue
Block a user