web: scope subagent storage to workspace profile
Subagent event JSONL files and rehydration metadata were stored in the shared ~/.openclaw/web-chat/ directory regardless of the active workspace profile, while parent chat sessions were correctly profile-scoped. Move subagent event persistence into the profile-scoped web-chat dir (web-chat-<profile>/subagent-events/) and add a profile-local subagent-index.json for fast rehydration after page refresh. The shared gateway registry (~/.openclaw/subagents/runs.json) remains the fallback source. Existing events in the legacy shared path are still readable as a migration fallback.
This commit is contained in:
parent
161c329214
commit
76f06e0eaf
@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
* Events are fed from the gateway WebSocket connection (gateway-events.ts).
|
* Events are fed from the gateway WebSocket connection (gateway-events.ts).
|
||||||
*/
|
*/
|
||||||
import { existsSync, readFileSync, mkdirSync, appendFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import {
|
import {
|
||||||
extractToolResult,
|
extractToolResult,
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
parseErrorBody,
|
parseErrorBody,
|
||||||
} from "./agent-runner";
|
} from "./agent-runner";
|
||||||
import { subscribeToSessionKey, type GatewayEvent } from "./gateway-events";
|
import { subscribeToSessionKey, type GatewayEvent } from "./gateway-events";
|
||||||
import { resolveOpenClawStateDir } from "./workspace";
|
import { resolveOpenClawStateDir, resolveWebChatDir } from "./workspace";
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
@ -84,7 +84,13 @@ function getRegistry(): SubagentRegistry {
|
|||||||
|
|
||||||
// ── Event persistence ──
|
// ── Event persistence ──
|
||||||
|
|
||||||
|
/** Profile-scoped directory for subagent event JSONL files. */
|
||||||
function subagentEventsDir(): string {
|
function subagentEventsDir(): string {
|
||||||
|
return join(resolveWebChatDir(), "subagent-events");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pre-profile-scoping legacy path — used as a read fallback for migration. */
|
||||||
|
function legacySubagentEventsDir(): string {
|
||||||
return join(resolveOpenClawStateDir(), "web-chat", "subagent-events");
|
return join(resolveOpenClawStateDir(), "web-chat", "subagent-events");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,8 +108,18 @@ function persistEvent(sessionKey: string, event: SseEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPersistedEvents(sessionKey: string): SseEvent[] {
|
function loadPersistedEvents(sessionKey: string): SseEvent[] {
|
||||||
const filePath = join(subagentEventsDir(), safeFilename(sessionKey));
|
const fname = safeFilename(sessionKey);
|
||||||
if (!existsSync(filePath)) {return [];}
|
|
||||||
|
// Try profile-scoped dir first, fall back to legacy shared dir.
|
||||||
|
let filePath = join(subagentEventsDir(), fname);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
const legacyPath = join(legacySubagentEventsDir(), fname);
|
||||||
|
if (existsSync(legacyPath)) {
|
||||||
|
filePath = legacyPath;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lines = readFileSync(filePath, "utf-8").split("\n");
|
const lines = readFileSync(filePath, "utf-8").split("\n");
|
||||||
@ -116,8 +132,50 @@ function loadPersistedEvents(sessionKey: string): SseEvent[] {
|
|||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Profile-scoped subagent index ──
|
||||||
|
|
||||||
|
type SubagentIndexEntry = {
|
||||||
|
runId: string;
|
||||||
|
parentWebSessionId: string;
|
||||||
|
task: string;
|
||||||
|
label?: string;
|
||||||
|
status: "running" | "completed" | "error";
|
||||||
|
startedAt: number;
|
||||||
|
endedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function subagentIndexPath(): string {
|
||||||
|
return join(resolveWebChatDir(), "subagent-index.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSubagentIndex(): Record<string, SubagentIndexEntry> {
|
||||||
|
const p = subagentIndexPath();
|
||||||
|
if (!existsSync(p)) {return {};}
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(p, "utf-8")) as Record<string, SubagentIndexEntry>;
|
||||||
|
} catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertSubagentIndex(sessionKey: string, entry: SubagentIndexEntry): void {
|
||||||
|
try {
|
||||||
|
const dir = resolveWebChatDir();
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const index = loadSubagentIndex();
|
||||||
|
index[sessionKey] = entry;
|
||||||
|
writeFileSync(subagentIndexPath(), JSON.stringify(index, null, 2));
|
||||||
|
} catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
/** Read the on-disk registry entry and derive the proper status. */
|
/** Read the on-disk registry entry and derive the proper status. */
|
||||||
function readDiskStatus(sessionKey: string): "running" | "completed" | "error" {
|
function readDiskStatus(sessionKey: string): "running" | "completed" | "error" {
|
||||||
|
// Check profile-scoped index first.
|
||||||
|
const profileIndex = loadSubagentIndex();
|
||||||
|
const profileEntry = profileIndex[sessionKey];
|
||||||
|
if (profileEntry) {
|
||||||
|
return profileEntry.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the shared gateway registry.
|
||||||
const registryPath = join(resolveOpenClawStateDir(), "subagents", "runs.json");
|
const registryPath = join(resolveOpenClawStateDir(), "subagents", "runs.json");
|
||||||
if (!existsSync(registryPath)) {return "running";}
|
if (!existsSync(registryPath)) {return "running";}
|
||||||
try {
|
try {
|
||||||
@ -188,6 +246,17 @@ export function registerSubagent(
|
|||||||
}
|
}
|
||||||
keys.add(info.sessionKey);
|
keys.add(info.sessionKey);
|
||||||
|
|
||||||
|
// Persist to the profile-scoped subagent index.
|
||||||
|
upsertSubagentIndex(info.sessionKey, {
|
||||||
|
runId: info.runId,
|
||||||
|
parentWebSessionId,
|
||||||
|
task: info.task,
|
||||||
|
label: info.label,
|
||||||
|
status: run.status,
|
||||||
|
startedAt: run.startedAt,
|
||||||
|
endedAt: run.endedAt,
|
||||||
|
});
|
||||||
|
|
||||||
// NOTE: We do NOT subscribe to gateway WebSocket here. During live
|
// NOTE: We do NOT subscribe to gateway WebSocket here. During live
|
||||||
// streaming, events arrive via routeRawEvent() from the parent's NDJSON
|
// streaming, events arrive via routeRawEvent() from the parent's NDJSON
|
||||||
// stream. After the parent exits, activateGatewayFallback() subscribes.
|
// stream. After the parent exits, activateGatewayFallback() subscribes.
|
||||||
@ -352,9 +421,10 @@ export function routeRawEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily register a subagent by reading the on-disk registry
|
* Lazily register a subagent by reading the on-disk registries.
|
||||||
* (~/.openclaw/subagents/runs.json). Returns true if the subagent was
|
* Checks the profile-scoped subagent-index.json first, then falls back
|
||||||
* found and registered (or was already registered).
|
* to the shared gateway registry (~/.openclaw/subagents/runs.json).
|
||||||
|
* Returns true if the subagent was found and registered (or already registered).
|
||||||
*/
|
*/
|
||||||
export function ensureRegisteredFromDisk(
|
export function ensureRegisteredFromDisk(
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
@ -362,6 +432,20 @@ export function ensureRegisteredFromDisk(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (getRegistry().runs.has(sessionKey)) {return true;}
|
if (getRegistry().runs.has(sessionKey)) {return true;}
|
||||||
|
|
||||||
|
// 1. Check profile-scoped index.
|
||||||
|
const profileIndex = loadSubagentIndex();
|
||||||
|
const profileEntry = profileIndex[sessionKey];
|
||||||
|
if (profileEntry) {
|
||||||
|
registerSubagent(profileEntry.parentWebSessionId || parentWebSessionId, {
|
||||||
|
sessionKey,
|
||||||
|
runId: profileEntry.runId,
|
||||||
|
task: profileEntry.task,
|
||||||
|
label: profileEntry.label,
|
||||||
|
}, { fromDisk: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to the shared gateway registry.
|
||||||
const registryPath = join(resolveOpenClawStateDir(), "subagents", "runs.json");
|
const registryPath = join(resolveOpenClawStateDir(), "subagents", "runs.json");
|
||||||
if (!existsSync(registryPath)) {return false;}
|
if (!existsSync(registryPath)) {return false;}
|
||||||
|
|
||||||
@ -572,6 +656,17 @@ function finalizeRun(run: SubagentRun, status: "completed" | "error"): void {
|
|||||||
run.status = status;
|
run.status = status;
|
||||||
run.endedAt = Date.now();
|
run.endedAt = Date.now();
|
||||||
|
|
||||||
|
// Update the profile-scoped subagent index with final status.
|
||||||
|
upsertSubagentIndex(run.sessionKey, {
|
||||||
|
runId: run.runId,
|
||||||
|
parentWebSessionId: run.parentWebSessionId,
|
||||||
|
task: run.task,
|
||||||
|
label: run.label,
|
||||||
|
status: run.status,
|
||||||
|
startedAt: run.startedAt,
|
||||||
|
endedAt: run.endedAt,
|
||||||
|
});
|
||||||
|
|
||||||
// Signal completion to all subscribers
|
// Signal completion to all subscribers
|
||||||
for (const sub of run.subscribers) {
|
for (const sub of run.subscribers) {
|
||||||
try { sub(null); } catch { /* ignore */ }
|
try { sub(null); } catch { /* ignore */ }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user