Files
openclaw/src/skills/runtime/refresh-state.ts
Vincent Koc 1884cedd35 fix(skills): refresh persisted snapshots after restart (#93513)
* fix(skills): refresh persisted snapshots after restart

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>

Co-authored-by: Oleksandr Zakotyanskyi <28755978+fif911@users.noreply.github.com>

Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-156600-autonomous-smoke (1)

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>

Co-authored-by: Oleksandr Zakotyanskyi <28755978+fif911@users.noreply.github.com>

Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>
2026-06-16 14:20:47 +08:00

92 lines
2.9 KiB
TypeScript

// Skill refresh state types describe change notifications emitted by runtime reloads.
export type SkillsChangeEvent = {
workspaceDir?: string;
reason: "watch" | "watch-targets" | "manual" | "remote-node" | "config-change" | "workshop";
changedPath?: string;
};
const listeners = new Set<(event: SkillsChangeEvent) => void>();
const workspaceVersions = new Map<string, number>();
const INITIAL_SKILLS_SNAPSHOT_VERSION = Date.now();
let globalVersion = INITIAL_SKILLS_SNAPSHOT_VERSION;
let listenerErrorHandler: ((err: unknown) => void) | undefined;
function bumpVersion(current: number): number {
const now = Date.now();
return now <= current ? current + 1 : now;
}
function emit(event: SkillsChangeEvent) {
for (const listener of listeners) {
try {
listener(event);
} catch (err) {
listenerErrorHandler?.(err);
}
}
}
export function setSkillsChangeListenerErrorHandler(handler?: (err: unknown) => void): void {
listenerErrorHandler = handler;
}
export function registerSkillsChangeListener(listener: (event: SkillsChangeEvent) => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function bumpSkillsSnapshotVersion(params?: {
workspaceDir?: string;
reason?: SkillsChangeEvent["reason"];
changedPath?: string;
}): number {
const reason = params?.reason ?? "manual";
const changedPath = params?.changedPath;
if (params?.workspaceDir) {
const current = Math.max(globalVersion, workspaceVersions.get(params.workspaceDir) ?? 0);
const next = bumpVersion(current);
workspaceVersions.set(params.workspaceDir, next);
emit({ workspaceDir: params.workspaceDir, reason, changedPath });
return next;
}
globalVersion = bumpVersion(globalVersion);
emit({ reason, changedPath });
return globalVersion;
}
export function getSkillsSnapshotVersion(workspaceDir?: string): number {
if (!workspaceDir) {
return globalVersion;
}
const local = workspaceVersions.get(workspaceDir) ?? 0;
return Math.max(globalVersion, local);
}
export function clearSkillsSnapshotVersionForWorkspace(workspaceDir: string): void {
const local = workspaceVersions.get(workspaceDir);
if (typeof local === "number" && local > globalVersion) {
// Keep pending workspace invalidation visible after dropping the workspace
// key; otherwise teardown can hide a skill change from cached snapshots.
globalVersion = local;
}
workspaceVersions.delete(workspaceDir);
}
export function shouldRefreshSnapshotForVersion(
cachedVersion?: number,
nextVersion?: number,
): boolean {
const cached = typeof cachedVersion === "number" ? cachedVersion : 0;
const next = typeof nextVersion === "number" ? nextVersion : 0;
return next === 0 ? cached > 0 : cached < next;
}
export function resetSkillsRefreshStateForTest(): void {
listeners.clear();
workspaceVersions.clear();
globalVersion = INITIAL_SKILLS_SNAPSHOT_VERSION;
listenerErrorHandler = undefined;
}