Skip to content

Commit 81abbc7

Browse files
committed
Fix cache edge cases
1 parent 5201179 commit 81abbc7

4 files changed

Lines changed: 89 additions & 1 deletion

File tree

src/agent-control-plugin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ export default function register(api: OpenClawPluginApi) {
189189
const syncAgent = async (state: AgentState): Promise<void> => {
190190
if (state.syncPromise) {
191191
await state.syncPromise;
192+
if (state.lastSyncedStepsHash !== state.stepsHash) {
193+
await syncAgent(state);
194+
}
192195
return;
193196
}
194197
if (state.lastSyncedStepsHash === state.stepsHash) {

src/session-store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ function resolveSessionAgentId(
135135
if (parts.length >= 2 && parts[0] === "agent" && parts[1]) {
136136
return parts[1];
137137
}
138-
return asString(sourceAgentId);
138+
const normalizedSourceAgentId = asString(sourceAgentId);
139+
return normalizedSourceAgentId === "default" ? undefined : normalizedSourceAgentId;
139140
}
140141

141142
function readSessionIdentityFromEntry(entry: Record<string, unknown>): SessionIdentitySnapshot {

test/agent-control-plugin.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,45 @@ describe("agent-control plugin logging and blocking", () => {
482482
expect(clientMocks.evaluationEvaluate).toHaveBeenCalledTimes(2);
483483
});
484484

485+
it("waits for catch-up sync before evaluating joined callers when steps change", async () => {
486+
// Given one tool call changes the step catalog while a joined caller waits on an in-flight sync
487+
const api = createMockApi({
488+
serverUrl: "http://localhost:8000",
489+
});
490+
const syncDeferred = createDeferred<void>();
491+
clientMocks.agentsInit
492+
.mockImplementationOnce(() => syncDeferred.promise)
493+
.mockResolvedValueOnce(undefined);
494+
resolveStepsForContextMock
495+
.mockResolvedValueOnce([{ type: "tool", name: "shell" }])
496+
.mockResolvedValueOnce([
497+
{ type: "tool", name: "shell" },
498+
{ type: "tool", name: "grep" },
499+
]);
500+
501+
// When the joined caller updates steps before the original sync resolves
502+
register(api.api);
503+
const first = runBeforeToolCall(api);
504+
await Promise.resolve();
505+
await Promise.resolve();
506+
const second = runBeforeToolCall(api, { toolName: "grep" });
507+
await Promise.resolve();
508+
await Promise.resolve();
509+
510+
expect(clientMocks.agentsInit).toHaveBeenCalledTimes(1);
511+
expect(clientMocks.evaluationEvaluate).not.toHaveBeenCalled();
512+
513+
syncDeferred.resolve(undefined);
514+
await Promise.all([first, second]);
515+
516+
// Then both callers wait until the catch-up sync completes before evaluating
517+
expect(clientMocks.agentsInit).toHaveBeenCalledTimes(2);
518+
expect(clientMocks.evaluationEvaluate).toHaveBeenCalledTimes(2);
519+
expect(clientMocks.evaluationEvaluate.mock.invocationCallOrder[0]).toBeGreaterThan(
520+
clientMocks.agentsInit.mock.invocationCallOrder[1] ?? 0,
521+
);
522+
});
523+
485524
it("skips resyncing when the step catalog has not changed", async () => {
486525
// Given a source agent whose step catalog is unchanged across two tool calls
487526
const api = createMockApi({

test/session-store.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,51 @@ describe("resolveSessionIdentity", () => {
299299
expect(mocks.importOpenClawInternalModule).not.toHaveBeenCalled();
300300
});
301301

302+
it("uses the OpenClaw default store for legacy keys when source agent is synthetic default", async () => {
303+
// Given the plugin fallback source agent ID is the synthetic default value
304+
const { resolveSessionIdentity, mocks } = await loadSessionStoreModule({
305+
throws: true,
306+
});
307+
const loadConfig = vi.fn(() => ({
308+
session: {
309+
store: "/tmp/{agentId}/sessions.json",
310+
},
311+
}));
312+
const resolveStorePath = vi.fn(
313+
(storePath?: string, opts?: { agentId?: string }) =>
314+
(storePath ?? "").replace("{agentId}", opts?.agentId ?? "main"),
315+
);
316+
const loadSessionStore = vi.fn(() => ({
317+
"legacy-session": {
318+
origin: {
319+
provider: "slack",
320+
chatType: "direct",
321+
label: "Alice",
322+
},
323+
},
324+
}));
325+
const api = createApi({ loadConfig, resolveStorePath, loadSessionStore });
326+
327+
// When identity is resolved for a non-agent-prefixed session key
328+
const identity = await resolveSessionIdentity({
329+
api,
330+
sourceAgentId: "default",
331+
sessionKey: "legacy-session",
332+
});
333+
334+
// Then the OpenClaw runtime resolves its own default agent store
335+
expect(identity).toMatchObject({
336+
provider: "slack",
337+
type: "direct",
338+
label: "Alice",
339+
});
340+
expect(resolveStorePath).toHaveBeenCalledWith("/tmp/{agentId}/sessions.json", {
341+
agentId: undefined,
342+
});
343+
expect(loadSessionStore).toHaveBeenCalledWith("/tmp/main/sessions.json");
344+
expect(mocks.importOpenClawInternalModule).not.toHaveBeenCalled();
345+
});
346+
302347
it("uses injected runtime helpers before falling back to internal imports", async () => {
303348
// Given OpenClaw provides session-store helpers through the plugin runtime
304349
const { resolveSessionIdentity, mocks } = await loadSessionStoreModule({

0 commit comments

Comments
 (0)