Persist generic binding touch updates

This commit is contained in:
Tak Hoffman
2026-04-10 18:58:06 -05:00
parent 58531530d9
commit fa040b41de
2 changed files with 46 additions and 18 deletions

View File

@@ -9,6 +9,7 @@ import {
bindGenericCurrentConversation,
getGenericCurrentConversationBindingCapabilities,
resolveGenericCurrentConversationBinding,
touchGenericCurrentConversationBinding,
unbindGenericCurrentConversationBindings,
} from "./current-conversation-bindings.js";
@@ -248,4 +249,41 @@ describe("generic current-conversation bindings", () => {
}),
).toBeNull();
});
it("persists touched activity across reloads", async () => {
const bound = await bindGenericCurrentConversation({
targetSessionKey: "agent:codex:acp:slack-dm",
targetKind: "session",
conversation: {
channel: "slack",
accountId: "default",
conversationId: "user:U123",
},
metadata: {
label: "slack-dm",
},
});
expect(bound).not.toBeNull();
touchGenericCurrentConversationBinding(
"generic:slack\u241fdefault\u241f\u241fuser:U123",
1_234_567_890,
);
__testing.resetCurrentConversationBindingsForTests();
expect(
resolveGenericCurrentConversationBinding({
channel: "slack",
accountId: "default",
conversationId: "user:U123",
})?.metadata,
).toEqual(
expect.objectContaining({
label: "slack-dm",
lastActivityAt: 1_234_567_890,
}),
);
});
});

View File

@@ -4,7 +4,7 @@ import { normalizeConversationText } from "../../acp/conversation-id.js";
import { normalizeAnyChannelId } from "../../channels/registry.js";
import { resolveStateDir } from "../../config/paths.js";
import { loadJsonFile } from "../../infra/json-file.js";
import { writeJsonFileAtomically } from "../../plugin-sdk/json-store.js";
import { saveJsonFile } from "../../plugin-sdk/json-store.js";
import { getActivePluginChannelRegistryFromState } from "../../plugins/runtime-state.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { normalizeConversationRef } from "./session-binding-normalization.js";
@@ -25,7 +25,6 @@ const CURRENT_BINDINGS_FILE_VERSION = 1;
const CURRENT_BINDINGS_ID_PREFIX = "generic:";
let bindingsLoaded = false;
let persistPromise: Promise<void> = Promise.resolve();
const bindingsByConversationKey = new Map<string, SessionBindingRecord>();
function buildConversationKey(ref: ConversationRef): string {
@@ -85,17 +84,8 @@ function loadBindingsIntoMemory(): void {
}
}
async function persistBindingsToDisk(): Promise<void> {
await writeJsonFileAtomically(resolveBindingsFilePath(), toPersistedFile());
}
function enqueuePersist(): Promise<void> {
persistPromise = persistPromise
.catch(() => {})
.then(async () => {
await persistBindingsToDisk();
});
return persistPromise;
function persistBindingsToDisk(): void {
saveJsonFile(resolveBindingsFilePath(), toPersistedFile());
}
function pruneExpiredBinding(key: string): SessionBindingRecord | null {
@@ -108,7 +98,7 @@ function pruneExpiredBinding(key: string): SessionBindingRecord | null {
return record;
}
bindingsByConversationKey.delete(key);
void enqueuePersist();
persistBindingsToDisk();
return null;
}
@@ -183,7 +173,7 @@ export async function bindGenericCurrentConversation(
},
};
bindingsByConversationKey.set(key, record);
await enqueuePersist();
persistBindingsToDisk();
return record;
}
@@ -225,6 +215,7 @@ export function touchGenericCurrentConversationBinding(bindingId: string, at = D
lastActivityAt: at,
},
});
persistBindingsToDisk();
}
export async function unbindGenericCurrentConversationBindings(
@@ -240,7 +231,7 @@ export async function unbindGenericCurrentConversationBindings(
if (record) {
bindingsByConversationKey.delete(key);
removed.push(record);
await enqueuePersist();
persistBindingsToDisk();
}
return removed;
}
@@ -256,7 +247,7 @@ export async function unbindGenericCurrentConversationBindings(
removed.push(record);
}
if (removed.length > 0) {
await enqueuePersist();
persistBindingsToDisk();
}
return removed;
}
@@ -268,7 +259,6 @@ export const __testing = {
}) {
bindingsLoaded = false;
bindingsByConversationKey.clear();
persistPromise = Promise.resolve();
if (params?.deletePersistedFile) {
const filePath = resolveBindingsFilePath(params.env);
try {