fix(sessions): hydrate agent-command skill snapshots

This commit is contained in:
Peter Steinberger
2026-05-02 12:31:02 +01:00
parent 479ed596bd
commit 05a5fa81a0
2 changed files with 103 additions and 29 deletions

View File

@@ -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<string, unknown> },
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<string, unknown> }
| 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) => {

View File

@@ -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();