mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:20:44 +00:00
refactor(skills): centralize snapshot hydration
This commit is contained in:
@@ -66,6 +66,7 @@ import {
|
||||
} from "./model-selection.js";
|
||||
import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
import { hydrateResolvedSkillsAsync } from "./skills/snapshot-hydration.js";
|
||||
import { normalizeSpawnedRunMetadata } from "./spawned-context.js";
|
||||
import { resolveAgentTimeoutMs } from "./timeout.js";
|
||||
import { ensureAgentWorkspace } from "./workspace.js";
|
||||
@@ -663,12 +664,7 @@ async function agentCommandInternal(
|
||||
? await buildSkillsSnapshot()
|
||||
: !currentSkillsSnapshot
|
||||
? undefined
|
||||
: currentSkillsSnapshot.resolvedSkills === undefined
|
||||
? {
|
||||
...currentSkillsSnapshot,
|
||||
resolvedSkills: (await buildSkillsSnapshot()).resolvedSkills,
|
||||
}
|
||||
: currentSkillsSnapshot;
|
||||
: await hydrateResolvedSkillsAsync(currentSkillsSnapshot, buildSkillsSnapshot);
|
||||
|
||||
if (skillsSnapshot && sessionStore && sessionKey && needsSkillsSnapshot) {
|
||||
const now = Date.now();
|
||||
|
||||
30
src/agents/skills/snapshot-hydration.ts
Normal file
30
src/agents/skills/snapshot-hydration.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
type SnapshotWithRuntimeSkills = {
|
||||
resolvedSkills?: unknown;
|
||||
};
|
||||
|
||||
type SnapshotRebuild<T extends SnapshotWithRuntimeSkills> = {
|
||||
resolvedSkills?: T["resolvedSkills"];
|
||||
};
|
||||
|
||||
// resolvedSkills is runtime-only: session persistence keeps the lightweight
|
||||
// catalog/prompt, while consumers that need concrete SKILL.md paths hydrate it
|
||||
// from a fresh workspace scan.
|
||||
export function hydrateResolvedSkills<T extends SnapshotWithRuntimeSkills>(
|
||||
snapshot: T,
|
||||
rebuild: () => SnapshotRebuild<T>,
|
||||
): T {
|
||||
if (snapshot.resolvedSkills !== undefined) {
|
||||
return snapshot;
|
||||
}
|
||||
return { ...snapshot, resolvedSkills: rebuild().resolvedSkills };
|
||||
}
|
||||
|
||||
export async function hydrateResolvedSkillsAsync<T extends SnapshotWithRuntimeSkills>(
|
||||
snapshot: T,
|
||||
rebuild: () => Promise<SnapshotRebuild<T>>,
|
||||
): Promise<T> {
|
||||
if (snapshot.resolvedSkills !== undefined) {
|
||||
return snapshot;
|
||||
}
|
||||
return { ...snapshot, resolvedSkills: (await rebuild()).resolvedSkills };
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
shouldRefreshSnapshotForVersion,
|
||||
} from "../../agents/skills/refresh-state.js";
|
||||
import { ensureSkillsWatcher } from "../../agents/skills/refresh.js";
|
||||
import { hydrateResolvedSkills } from "../../agents/skills/snapshot-hydration.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
@@ -104,22 +105,6 @@ function resolvePositiveTokenCount(value: number | undefined): number | undefine
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// resolvedSkills is stripped from the persisted snapshot (see store-load.ts).
|
||||
// On cold session resume, the snapshot loaded from disk reaches this code path
|
||||
// without resolvedSkills. Consumers like prepareClaudeCliSkillsPlugin and the
|
||||
// claude-live-session fingerprint read resolvedSkills directly, so re-fill it
|
||||
// here from a fresh workspace scan while preserving the persisted prompt /
|
||||
// skills / version fields for prompt-cache stability.
|
||||
export function hydrateResolvedSkills(
|
||||
snapshot: NonNullable<SessionEntry["skillsSnapshot"]>,
|
||||
rebuild: () => NonNullable<SessionEntry["skillsSnapshot"]>,
|
||||
): NonNullable<SessionEntry["skillsSnapshot"]> {
|
||||
if (snapshot.resolvedSkills) {
|
||||
return snapshot;
|
||||
}
|
||||
return { ...snapshot, resolvedSkills: rebuild().resolvedSkills };
|
||||
}
|
||||
|
||||
export async function ensureSkillSnapshot(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
|
||||
@@ -4,7 +4,10 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
|
||||
import { resolveEmbeddedRunSkillEntries } from "../../agents/pi-embedded-runner/skills-runtime.js";
|
||||
import { createCanonicalFixtureSkill } from "../../agents/skills.test-helpers.js";
|
||||
import type { Skill } from "../../agents/skills/skill-contract.js";
|
||||
import { hydrateResolvedSkills } from "../../auto-reply/reply/session-updates.js";
|
||||
import {
|
||||
hydrateResolvedSkills,
|
||||
hydrateResolvedSkillsAsync,
|
||||
} from "../../agents/skills/snapshot-hydration.js";
|
||||
import { createSuiteTempRootTracker } from "../../test-helpers/temp-dir.js";
|
||||
import type { SessionEntry, SessionSkillSnapshot } from "./types.js";
|
||||
|
||||
@@ -269,4 +272,23 @@ describe("hydrateResolvedSkills", () => {
|
||||
expect(result).toBe(snapshot);
|
||||
expect(buildCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("supports async runtime hydration for CLI resume paths", async () => {
|
||||
const stripped: SessionSkillSnapshot = {
|
||||
prompt: "cached-prompt",
|
||||
skills: [{ name: "x" }],
|
||||
version: 2,
|
||||
};
|
||||
const rebuiltSkills = [makeFixtureSkill("x", 120)];
|
||||
const result = await hydrateResolvedSkillsAsync(stripped, async () => ({
|
||||
prompt: "fresh-prompt",
|
||||
skills: [{ name: "y" }],
|
||||
resolvedSkills: rebuiltSkills,
|
||||
version: 3,
|
||||
}));
|
||||
expect(result.prompt).toBe("cached-prompt");
|
||||
expect(result.skills).toEqual([{ name: "x" }]);
|
||||
expect(result.version).toBe(2);
|
||||
expect(result.resolvedSkills).toBe(rebuiltSkills);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user