refactor: migrate threads index from YAML to ocas variable store (Phase 4b)
CI / check (pull_request) Failing after 12m38s
CI / check (pull_request) Failing after 12m38s
- Replace loadThreadsIndex/saveThreadsIndex with granular variable API: loadAllThreads, getThread, setThread, deleteThread - Variable: @uwf/thread/<thread-id>, value=head hash, tags=suspend metadata - Auto-migration: threads.yaml → variables, renames to .migrated - Updated ~20 call sites in thread.ts, step.ts, shared.ts - workflow-util-agent: getActiveThreadEntry reads from variable store - New test helper: seedThread/seedThreads - biome fix: removed unused imports - 22 files changed Ref #11
This commit is contained in:
@@ -7,7 +7,7 @@ import type {
|
||||
ThreadId,
|
||||
} from "@united-workforce/protocol";
|
||||
import type { AgentStore } from "./storage.js";
|
||||
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
|
||||
import { createAgentStore, getActiveThreadEntry, resolveStorageRoot } from "./storage.js";
|
||||
import type { AgentContext } from "./types.js";
|
||||
|
||||
type ChainState = {
|
||||
@@ -162,13 +162,12 @@ export async function buildContext(
|
||||
const agentStore = await createAgentStore(storageRoot);
|
||||
const { store, schemas } = agentStore;
|
||||
|
||||
const index = await loadThreadsIndex(storageRoot);
|
||||
const headHash = index[threadId]?.head;
|
||||
if (headHash === undefined) {
|
||||
fail(`thread not found in threads.yaml: ${threadId}`);
|
||||
const entry = await getActiveThreadEntry(storageRoot, threadId);
|
||||
if (entry === null) {
|
||||
fail(`thread not found in active thread index: ${threadId}`);
|
||||
}
|
||||
|
||||
const chain = walkChain(store, schemas, headHash);
|
||||
const chain = walkChain(store, schemas, entry.head);
|
||||
const workflow = await loadWorkflow(store, schemas, chain.start.workflow);
|
||||
const roleDef = workflow.roles[role];
|
||||
if (roleDef === undefined) {
|
||||
@@ -211,13 +210,12 @@ export async function buildContextWithMeta(
|
||||
const agentStore = await createAgentStore(storageRoot);
|
||||
const { store, schemas } = agentStore;
|
||||
|
||||
const index = await loadThreadsIndex(storageRoot);
|
||||
const headHash = index[threadId]?.head;
|
||||
if (headHash === undefined) {
|
||||
fail(`thread not found in threads.yaml: ${threadId}`);
|
||||
const entry = await getActiveThreadEntry(storageRoot, threadId);
|
||||
if (entry === null) {
|
||||
fail(`thread not found in active thread index: ${threadId}`);
|
||||
}
|
||||
|
||||
const chain = walkChain(store, schemas, headHash);
|
||||
const chain = walkChain(store, schemas, entry.head);
|
||||
const workflow = await loadWorkflow(store, schemas, chain.start.workflow);
|
||||
const roleDef = workflow.roles[role];
|
||||
if (roleDef === undefined) {
|
||||
@@ -237,6 +235,6 @@ export async function buildContextWithMeta(
|
||||
outputFormatInstruction: "",
|
||||
edgePrompt,
|
||||
isFirstVisit,
|
||||
meta: { storageRoot, store, schemas, headHash, chain },
|
||||
meta: { storageRoot, store, schemas, headHash: entry.head, chain },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,21 +2,22 @@ import { readFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { Store } from "@ocas/core";
|
||||
import { createVariableStore, type Store } from "@ocas/core";
|
||||
import { createFsStore } from "@ocas/fs";
|
||||
import type {
|
||||
AgentAlias,
|
||||
AgentConfig,
|
||||
CasRef,
|
||||
ModelAlias,
|
||||
ModelConfig,
|
||||
ProviderAlias,
|
||||
ProviderConfig,
|
||||
Scenario,
|
||||
ThreadsIndex,
|
||||
ThreadId,
|
||||
ThreadIndexEntry,
|
||||
WorkflowConfig,
|
||||
WorkflowName,
|
||||
} from "@united-workforce/protocol";
|
||||
import { parseThreadsIndex } from "@united-workforce/protocol";
|
||||
import { parse } from "yaml";
|
||||
|
||||
import { registerAgentSchemas } from "./schemas.js";
|
||||
@@ -58,8 +59,46 @@ export function getEnvPath(storageRoot: string): string {
|
||||
return join(storageRoot, ".env");
|
||||
}
|
||||
|
||||
export function getThreadsPath(storageRoot: string): string {
|
||||
return join(storageRoot, "threads.yaml");
|
||||
const THREAD_VAR_PREFIX = "@uwf/thread/";
|
||||
|
||||
/**
|
||||
* Global CAS directory (same as uwf CLI).
|
||||
* Priority: `OCAS_DIR` → `UNCAGED_CAS_DIR` (legacy) → default ~/.ocas
|
||||
*/
|
||||
export function getGlobalCasDir(): string {
|
||||
const primary = process.env.OCAS_DIR;
|
||||
if (primary !== undefined && primary !== "") {
|
||||
return primary;
|
||||
}
|
||||
const legacy = process.env.UNCAGED_CAS_DIR;
|
||||
if (legacy !== undefined && legacy !== "") {
|
||||
return legacy;
|
||||
}
|
||||
return join(homedir(), ".ocas");
|
||||
}
|
||||
|
||||
function threadVarName(threadId: ThreadId): string {
|
||||
return `${THREAD_VAR_PREFIX}${threadId}`;
|
||||
}
|
||||
|
||||
/** Read active thread head + suspend metadata from ocas variable store. */
|
||||
export async function getActiveThreadEntry(
|
||||
_storageRoot: string,
|
||||
threadId: ThreadId,
|
||||
): Promise<ThreadIndexEntry | null> {
|
||||
const casDir = getGlobalCasDir();
|
||||
const store = createFsStore(casDir);
|
||||
const varStore = createVariableStore(join(casDir, "variables.db"), store);
|
||||
const vars = varStore.list({ exactName: threadVarName(threadId) });
|
||||
const v = vars[0];
|
||||
if (v === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
head: v.value as CasRef,
|
||||
suspendedRole: v.tags.suspendedRole ?? null,
|
||||
suspendMessage: v.tags.suspendMessage ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export type AgentStore = {
|
||||
@@ -205,18 +244,3 @@ export async function loadWorkflowConfig(storageRoot: string): Promise<WorkflowC
|
||||
const raw = parse(text) as unknown;
|
||||
return normalizeWorkflowConfig(raw);
|
||||
}
|
||||
|
||||
export async function loadThreadsIndex(storageRoot: string): Promise<ThreadsIndex> {
|
||||
const path = getThreadsPath(storageRoot);
|
||||
try {
|
||||
const text = await readFile(path, "utf8");
|
||||
const raw = parse(text) as unknown;
|
||||
return parseThreadsIndex(raw);
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
if (err.code === "ENOENT") {
|
||||
return {};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user