From 05a5fa81a0e612430afd9f3b61cc9a4851a84253 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 12:31:02 +0100 Subject: [PATCH] fix(sessions): hydrate agent-command skill snapshots --- .../agent-command.live-model-switch.test.ts | 68 ++++++++++++++++++- src/agents/agent-command.ts | 64 +++++++++-------- 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index e3685dcf9b2..0fe3b9df71b 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -37,6 +37,12 @@ const state = vi.hoisted(() => ({ isThinkingLevelSupportedMock: vi.fn((_args: unknown) => true), resolveThinkingDefaultMock: vi.fn((_args: unknown) => "low"), loadManifestModelCatalogMock: vi.fn(() => []), + buildWorkspaceSkillSnapshotMock: vi.fn((..._args: unknown[]): unknown => ({ + prompt: "", + skills: [], + resolvedSkills: [], + version: 0, + })), authProfileStoreMock: { profiles: {} } as { profiles: Record }, sessionEntryMock: undefined as unknown, sessionStoreMock: undefined as unknown, @@ -415,7 +421,8 @@ vi.mock("./provider-auth-aliases.js", () => ({ })); vi.mock("./skills.js", () => ({ - buildWorkspaceSkillSnapshot: () => ({}), + buildWorkspaceSkillSnapshot: (workspaceDir: string, opts: unknown) => + state.buildWorkspaceSkillSnapshotMock(workspaceDir, opts), })); vi.mock("./skills/filter.js", () => ({ @@ -565,6 +572,12 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { state.authProfileStoreMock = { profiles: {} }; state.sessionEntryMock = undefined; state.sessionStoreMock = undefined; + state.buildWorkspaceSkillSnapshotMock.mockReturnValue({ + prompt: "", + skills: [], + resolvedSkills: [], + version: 0, + }); state.deliverAgentCommandResultMock.mockResolvedValue(undefined); state.updateSessionStoreAfterAgentRunMock.mockResolvedValue(undefined); state.trajectoryFlushMock.mockResolvedValue(undefined); @@ -844,6 +857,59 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { expect(state.clearSessionAuthProfileOverrideMock).not.toHaveBeenCalled(); }); + it("hydrates stripped persisted skill snapshots before running the CLI path", async () => { + const persistedSnapshot = { + prompt: "persisted prompt", + skills: [{ name: "cli-skill" }], + skillFilter: ["cli-skill"], + version: 0, + }; + const rebuiltSkills = [ + { + name: "cli-skill", + description: "CLI skill", + filePath: "/tmp/workspace/skills/cli-skill/SKILL.md", + baseDir: "/tmp/workspace/skills/cli-skill", + source: "# CLI skill", + }, + ]; + state.sessionEntryMock = { + sessionId: "session-1", + updatedAt: Date.now(), + skillsSnapshot: persistedSnapshot, + }; + state.buildWorkspaceSkillSnapshotMock.mockReturnValue({ + prompt: "rebuilt prompt", + skills: [{ name: "different-skill" }], + resolvedSkills: rebuiltSkills, + version: 99, + }); + state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => { + const result = await params.run(params.provider, params.model); + return { + result, + provider: params.provider, + model: params.model, + attempts: [], + }; + }); + state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude")); + + await runBasicAgentCommand(); + + const attemptParams = state.runAgentAttemptMock.mock.calls[0]?.[0] as + | { skillsSnapshot?: Record } + | undefined; + expect(attemptParams?.skillsSnapshot).toMatchObject({ + prompt: "persisted prompt", + skills: [{ name: "cli-skill" }], + skillFilter: ["cli-skill"], + version: 0, + resolvedSkills: rebuiltSkills, + }); + expect(state.buildWorkspaceSkillSnapshotMock).toHaveBeenCalledTimes(1); + }); + it("classifies empty embedded run results before model fallback accepts them", async () => { let observedClassification: unknown; state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => { diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 1e2919832ea..300e71a7193 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -632,35 +632,43 @@ async function agentCommandInternal( shouldRefreshSnapshotForVersion(currentSkillsSnapshot.version, skillsSnapshotVersion) || !matchesSkillFilter(currentSkillsSnapshot.skillFilter, skillFilter); const needsSkillsSnapshot = isNewSession || shouldRefreshSkillsSnapshot; + const buildSkillsSnapshot = async () => { + const [ + { buildWorkspaceSkillSnapshot }, + { getRemoteSkillEligibility }, + { canExecRequestNode }, + ] = await Promise.all([ + loadSkillsRuntime(), + loadSkillsRemoteRuntime(), + loadExecDefaultsRuntime(), + ]); + return buildWorkspaceSkillSnapshot(workspaceDir, { + config: cfg, + eligibility: { + remote: getRemoteSkillEligibility({ + advertiseExecNode: canExecRequestNode({ + cfg, + sessionEntry, + sessionKey, + agentId: sessionAgentId, + }), + }), + }, + snapshotVersion: skillsSnapshotVersion, + skillFilter, + agentId: sessionAgentId, + }); + }; const skillsSnapshot = needsSkillsSnapshot - ? await (async () => { - const [ - { buildWorkspaceSkillSnapshot }, - { getRemoteSkillEligibility }, - { canExecRequestNode }, - ] = await Promise.all([ - loadSkillsRuntime(), - loadSkillsRemoteRuntime(), - loadExecDefaultsRuntime(), - ]); - return buildWorkspaceSkillSnapshot(workspaceDir, { - config: cfg, - eligibility: { - remote: getRemoteSkillEligibility({ - advertiseExecNode: canExecRequestNode({ - cfg, - sessionEntry, - sessionKey, - agentId: sessionAgentId, - }), - }), - }, - snapshotVersion: skillsSnapshotVersion, - skillFilter, - agentId: sessionAgentId, - }); - })() - : currentSkillsSnapshot; + ? await buildSkillsSnapshot() + : !currentSkillsSnapshot + ? undefined + : currentSkillsSnapshot.resolvedSkills === undefined + ? { + ...currentSkillsSnapshot, + resolvedSkills: (await buildSkillsSnapshot()).resolvedSkills, + } + : currentSkillsSnapshot; if (skillsSnapshot && sessionStore && sessionKey && needsSkillsSnapshot) { const now = Date.now();