refactor: migrate threads index from YAML to ocas variable store (Phase 4b)
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:
2026-06-02 22:22:38 +08:00
parent 9326cc430f
commit 93b96987a3
22 changed files with 419 additions and 283 deletions
+10 -12
View File
@@ -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 },
};
}
+44 -20
View File
@@ -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;
}
}