Skip to content

Commit f2530de

Browse files
openperfclawsweeper[bot]Takhoffman
authored
fix(agents): do not refresh lastUsedAt on MCP lease release (#91124)
Summary: - The PR removes release-time `lastUsedAt` refresh from session MCP runtime lease cleanup and adds regression tests for idle eviction after a lease expires while active. - PR surface: Source 0, Tests +74. Total +74 across 2 files. - Reproducibility: yes. from source inspection: current main refreshes `lastUsedAt` in the release callback, a ... timestamp after active leases drop to zero. I did not execute the focused Vitest in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(agents): do not refresh lastUsedAt on MCP lease release Validation: - ClawSweeper review passed for head c914478. - Required merge gates passed before the squash merge. Prepared head SHA: c914478 Review: #91124 (comment) Co-authored-by: openperf <[email protected]> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <[email protected]>
1 parent 75c1790 commit f2530de

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

src/agents/agent-bundle-mcp-runtime.test.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
77
import { createBundleMcpJsonSchemaValidator } from "./agent-bundle-mcp-runtime.js";
88
import { cleanupBundleMcpHarness } from "./agent-bundle-mcp-test-harness.js";
99
import {
10-
testing,
10+
createSessionMcpRuntime,
1111
getOrCreateSessionMcpRuntime,
1212
materializeBundleMcpToolsForRun,
1313
retireSessionMcpRuntime,
1414
retireSessionMcpRuntimeForSessionKey,
15+
testing,
1516
} from "./agent-bundle-mcp-tools.js";
1617
import type { SessionMcpRuntime } from "./agent-bundle-mcp-types.js";
1718
import { writeExecutable } from "./bundle-mcp-shared.test-harness.js";
@@ -1591,7 +1592,6 @@ process.on("SIGINT", shutdown);`,
15911592
activeLeases += 1;
15921593
return () => {
15931594
activeLeases -= 1;
1594-
lastUsedAt = now;
15951595
};
15961596
},
15971597
dispose: async () => {
@@ -1626,6 +1626,65 @@ process.on("SIGINT", shutdown);`,
16261626
expect(manager.resolveSessionId("agent:test:session-idle")).toBeUndefined();
16271627
});
16281628

1629+
it("evicts immediately after release when TTL already elapsed during active lease", async () => {
1630+
let now = 1_000;
1631+
const disposed: string[] = [];
1632+
const createRuntime: RuntimeFactory = (params) => {
1633+
let lastUsedAt = now;
1634+
let activeLeases = 0;
1635+
return {
1636+
...makeRuntime([{ toolName: "bundle_probe", description: "Bundle MCP probe" }]),
1637+
sessionId: params.sessionId,
1638+
sessionKey: params.sessionKey,
1639+
workspaceDir: params.workspaceDir,
1640+
configFingerprint: params.configFingerprint ?? "fingerprint",
1641+
get lastUsedAt() {
1642+
return lastUsedAt;
1643+
},
1644+
get activeLeases() {
1645+
return activeLeases;
1646+
},
1647+
markUsed: () => {
1648+
lastUsedAt = now;
1649+
},
1650+
acquireLease: () => {
1651+
activeLeases += 1;
1652+
return () => {
1653+
activeLeases -= 1;
1654+
};
1655+
},
1656+
dispose: async () => {
1657+
disposed.push(params.sessionId);
1658+
},
1659+
};
1660+
};
1661+
const manager = testing.createSessionMcpRuntimeManager({
1662+
createRuntime,
1663+
now: () => now,
1664+
enableIdleSweepTimer: false,
1665+
});
1666+
1667+
const runtime = await manager.getOrCreate({
1668+
sessionId: "session-idle-post-release",
1669+
sessionKey: "agent:test:session-idle-post-release",
1670+
workspaceDir: "/workspace",
1671+
cfg: { mcp: { servers: {}, sessionIdleTtlMs: 50 } },
1672+
});
1673+
const releaseLease = runtime.acquireLease?.();
1674+
1675+
// TTL elapses while the lease is still held, so sweep skips active runtimes.
1676+
now += 60;
1677+
await expect(manager.sweepIdleRuntimes()).resolves.toBe(0);
1678+
1679+
// Release must not reset lastUsedAt; the runtime is evictable on the very
1680+
// next sweep without waiting another full TTL.
1681+
releaseLease?.();
1682+
await expect(manager.sweepIdleRuntimes()).resolves.toBe(1);
1683+
1684+
expect(disposed).toEqual(["session-idle-post-release"]);
1685+
expect(manager.listSessionIds()).toStrictEqual([]);
1686+
});
1687+
16291688
it("keeps idle runtime eviction disabled when the TTL is zero", async () => {
16301689
let now = 1_000;
16311690
const disposed: string[] = [];
@@ -1655,6 +1714,21 @@ process.on("SIGINT", shutdown);`,
16551714
expect(manager.listSessionIds()).toEqual(["session-no-ttl"]);
16561715
expect(disposed).toStrictEqual([]);
16571716
});
1717+
1718+
it("production createSessionMcpRuntime acquireLease release does not refresh lastUsedAt", () => {
1719+
const runtime = createSessionMcpRuntime({
1720+
sessionId: "session-lease-timestamp-check",
1721+
workspaceDir: "/workspace",
1722+
cfg: { mcp: { servers: {} } },
1723+
});
1724+
const lastUsedBefore = runtime.lastUsedAt;
1725+
if (!runtime.acquireLease) {
1726+
throw new Error("Expected production session MCP runtime to expose acquireLease");
1727+
}
1728+
const release = runtime.acquireLease();
1729+
release();
1730+
expect(runtime.lastUsedAt).toBe(lastUsedBefore);
1731+
});
16581732
});
16591733

16601734
describe("disposeSession timeout", () => {

src/agents/agent-bundle-mcp-runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ export function createSessionMcpRuntime(params: {
765765
}
766766
released = true;
767767
activeLeases = Math.max(0, activeLeases - 1);
768-
lastUsedAt = Date.now();
768+
// Release is not use: refreshing lastUsedAt here defeats the idle-sweep TTL.
769769
};
770770
},
771771
getCatalog,

0 commit comments

Comments
 (0)