refactor: remove transcript file mapping table

This commit is contained in:
Peter Steinberger
2026-05-09 05:46:26 +01:00
parent d8aa331103
commit f85f8df64e
12 changed files with 52 additions and 99 deletions

View File

@@ -103,16 +103,16 @@ The branch already has a real shared SQLite base:
file-to-database import remains in doctor code, and branch-local
database upgrade helpers have been deleted.
- Relational ownership is enforced where the ownership boundary is canonical:
transcript-file mappings cascade from `agent_databases`, source migration
rows cascade from `migration_runs`, task delivery state cascades from
`task_runs`, and transcript identity rows cascade from transcript events.
source migration rows cascade from `migration_runs`, task delivery state
cascades from `task_runs`, and transcript identity rows cascade from
transcript events.
- Current shared tables include `kv`, `agents`, `agent_databases`,
`plugin_state_entries`, `plugin_blob_entries`, `transcript_files`,
`capture_sessions`, `capture_events`, `capture_blobs`,
`sandbox_registry_entries`, `cron_run_logs`, `cron_jobs`, `commitments`,
`delivery_queue_entries`, `current_conversation_bindings`,
`tui_last_sessions`, `task_runs`, `task_delivery_state`, `flow_runs`,
`subagent_runs`, `migration_runs`, and `backup_runs`.
`plugin_state_entries`, `plugin_blob_entries`, `capture_sessions`,
`capture_events`, `capture_blobs`, `sandbox_registry_entries`,
`cron_run_logs`, `cron_jobs`, `commitments`, `delivery_queue_entries`,
`current_conversation_bindings`, `tui_last_sessions`, `task_runs`,
`task_delivery_state`, `flow_runs`, `subagent_runs`, `migration_runs`, and
`backup_runs`.
- `src/state/openclaw-agent-db.ts` opens
`agents/<agentId>/agent/openclaw-agent.sqlite`, registers the database in the
global DB, and owns agent-local session, transcript, VFS, artifact, and cache

View File

@@ -364,7 +364,6 @@ kv(scope, key, value_json, updated_at)
agents(agent_id, config_json, created_at, updated_at)
session_entries(agent_id, session_key, entry_json, updated_at)
transcript_events(agent_id, session_id, seq, event_json, created_at)
transcript_files(agent_id, session_id, path, imported_at, exported_at)
vfs_entries(agent_id, namespace, path, kind, content_blob, metadata_json, updated_at)
tool_artifacts(agent_id, run_id, artifact_id, kind, metadata_json, blob, created_at)
```

View File

@@ -83,9 +83,9 @@ Per agent, on the Gateway host:
sources after durable verification. Gateway startup leaves legacy indexes
alone.
- Transcripts: runtime transcript events live in the per-agent database
(`transcript_events` and `transcript_event_identities`). The global
`transcript_files` table maps legacy/export/debug path-shaped locators to
`{ agentId, sessionId }`; JSONL files are not runtime sidecars.
(`transcript_events` and `transcript_event_identities`). Session file values
are canonical `sqlite-transcript://<agentId>/<sessionId>.jsonl` locators;
JSONL files are doctor migration inputs, not runtime sidecars.
- Telegram topic handles: `.../<sessionId>-topic-<threadId>.jsonl`
OpenClaw resolves these via `src/config/sessions/*`.

View File

@@ -18,7 +18,7 @@ import {
type SessionEntry,
} from "../../config/sessions.js";
import {
listSqliteSessionTranscriptFiles,
listSqliteSessionTranscriptLocators,
loadSqliteSessionTranscriptEvents,
resolveSqliteSessionTranscriptScope,
} from "../../config/sessions/transcript-store.sqlite.js";
@@ -233,7 +233,7 @@ function resolveSqliteSessionTranscriptPath(params: {
return undefined;
}
const agentId = params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined;
const candidates = listSqliteSessionTranscriptFiles().filter(
const candidates = listSqliteSessionTranscriptLocators().filter(
(entry) => (!agentId || entry.agentId === agentId) && entry.sessionId === sessionId,
);
if (candidates.length === 0) {

View File

@@ -6,10 +6,7 @@ import {
closeOpenClawAgentDatabasesForTest,
openOpenClawAgentDatabase,
} from "../../state/openclaw-agent-db.js";
import {
closeOpenClawStateDatabaseForTest,
openOpenClawStateDatabase,
} from "../../state/openclaw-state-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { createSqliteSessionTranscriptLocator } from "./paths.js";
import {
appendSqliteSessionTranscriptEvent,
@@ -206,24 +203,6 @@ describe("SQLite session transcript store", () => {
]);
});
it("does not write runtime transcript file mappings", () => {
const stateDir = createTempDir();
appendSqliteSessionTranscriptEvent({
env: { OPENCLAW_STATE_DIR: stateDir },
agentId: "main",
sessionId: "session-1",
transcriptPath: path.join(stateDir, "session.jsonl"),
event: { type: "session", id: "session-1" },
});
const stateDatabase = openOpenClawStateDatabase({
env: { OPENCLAW_STATE_DIR: stateDir },
});
expect(
stateDatabase.db.prepare("SELECT COUNT(*) AS count FROM transcript_files").get(),
).toEqual({ count: 0 });
});
it("deletes transcript snapshots with the transcript", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };

View File

@@ -57,7 +57,7 @@ export type SqliteSessionTranscriptScope = {
sessionId: string;
};
export type SqliteSessionTranscriptFile = SqliteSessionTranscriptScope & {
export type SqliteSessionTranscriptLocator = SqliteSessionTranscriptScope & {
path: string;
updatedAt: number;
};
@@ -116,6 +116,12 @@ function getAgentTranscriptKysely(db: import("node:sqlite").DatabaseSync) {
return getNodeSqliteKysely<AgentTranscriptDatabase>(db);
}
function openTranscriptAgentDatabase(
options: SqliteSessionTranscriptStoreOptions,
): OpenClawAgentDatabase {
return openOpenClawAgentDatabase({ env: options.env, agentId: options.agentId });
}
function bindTranscriptEvent(params: {
sessionId: string;
seq: number;
@@ -265,9 +271,9 @@ export function resolveSqliteSessionTranscriptScope(
};
}
export function listSqliteSessionTranscriptFiles(
export function listSqliteSessionTranscriptLocators(
options: OpenClawStateDatabaseOptions = {},
): SqliteSessionTranscriptFile[] {
): SqliteSessionTranscriptLocator[] {
return listSqliteSessionTranscripts(options).map((transcript) => ({
agentId: transcript.agentId,
sessionId: transcript.sessionId,
@@ -352,7 +358,7 @@ export function getSqliteSessionTranscriptStats(
options: SqliteSessionTranscriptStoreOptions,
): Pick<SqliteSessionTranscript, "sessionId" | "updatedAt" | "eventCount"> | null {
const { sessionId } = normalizeTranscriptScope(options);
const database = openOpenClawAgentDatabase(options);
const database = openTranscriptAgentDatabase(options);
const row = executeSqliteQueryTakeFirstSync(
database.db,
getAgentTranscriptKysely(database.db)
@@ -519,7 +525,7 @@ export function loadSqliteSessionTranscriptEvents(
options: SqliteSessionTranscriptStoreOptions,
): SqliteSessionTranscriptEvent[] {
const { sessionId } = normalizeTranscriptScope(options);
const database = openOpenClawAgentDatabase(options);
const database = openTranscriptAgentDatabase(options);
return executeSqliteQuerySync(
database.db,
getAgentTranscriptKysely(database.db)
@@ -542,7 +548,7 @@ export function hasSqliteSessionTranscriptEvents(
options: SqliteSessionTranscriptStoreOptions,
): boolean {
const { sessionId } = normalizeTranscriptScope(options);
const database = openOpenClawAgentDatabase(options);
const database = openTranscriptAgentDatabase(options);
const row = executeSqliteQueryTakeFirstSync(
database.db,
getAgentTranscriptKysely(database.db)
@@ -598,7 +604,7 @@ export function hasSqliteSessionTranscriptSnapshot(
): boolean {
const { sessionId } = normalizeTranscriptScope(options);
const snapshotId = normalizeSessionId(options.snapshotId);
const database = openOpenClawAgentDatabase(options);
const database = openTranscriptAgentDatabase(options);
const row = executeSqliteQueryTakeFirstSync(
database.db,
getAgentTranscriptKysely(database.db)

View File

@@ -4,10 +4,7 @@ import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as transcriptEvents from "../../sessions/transcript-events.js";
import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js";
import {
closeOpenClawStateDatabaseForTest,
openOpenClawStateDatabase,
} from "../../state/openclaw-state-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { createSqliteSessionTranscriptLocator } from "./paths.js";
import { upsertSessionEntry } from "./store.js";
import { useTempSessionsFixture } from "./test-helpers.js";
@@ -632,11 +629,6 @@ describe("appendAssistantMessageToSessionTranscript", () => {
}).map((entry) => entry.event as { type?: string; message?: unknown });
expect(events.map((event) => event.type)).toEqual(["session", "message"]);
const stateDatabase = openOpenClawStateDatabase({ env });
expect(
stateDatabase.db.prepare("SELECT COUNT(*) AS count FROM transcript_files").get(),
).toEqual({ count: 0 });
fs.rmSync(stateDir, { recursive: true, force: true });
});

View File

@@ -1,6 +1,7 @@
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { replaceSqliteSessionTranscriptEvents } from "../config/sessions/transcript-store.sqlite.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
@@ -32,8 +33,8 @@ describe("session cost usage", () => {
const makeRoot = async (prefix: string): Promise<string> => await suiteRootTracker.make(prefix);
const sessionPath = (root: string, sessionId: string, agentId = "main") =>
path.join(root, "agents", agentId, "sessions", `${sessionId}.jsonl`);
const sessionPath = (_root: string, sessionId: string, agentId = "main") =>
createSqliteSessionTranscriptLocator({ agentId, sessionId });
const writeTranscript = (params: {
agentId?: string;
@@ -41,11 +42,20 @@ describe("session cost usage", () => {
transcriptPath?: string;
events: unknown[];
}) => {
const eventTimestamp = params.events
.map((event) =>
event && typeof event === "object"
? Date.parse(String((event as { timestamp?: unknown }).timestamp ?? ""))
: NaN,
)
.find((value) => Number.isFinite(value));
replaceSqliteSessionTranscriptEvents({
agentId: params.agentId ?? "main",
sessionId: params.sessionId,
transcriptPath: params.transcriptPath,
transcriptPath:
params.transcriptPath ?? sessionPath("", params.sessionId, params.agentId ?? "main"),
events: [{ type: "session", version: 1, id: params.sessionId }, ...params.events],
...(eventTimestamp !== undefined ? { now: () => eventTimestamp } : {}),
});
};
@@ -61,6 +71,14 @@ describe("session cost usage", () => {
}) => ({
type: "message",
timestamp: params.timestamp,
provider: params.provider ?? "openai",
model: params.model ?? "gpt-5.4",
usage: {
input: params.input,
output: params.output,
totalTokens: params.totalTokens ?? params.input + params.output,
...(params.cost === undefined ? {} : { cost: { total: params.cost } }),
},
message: {
role: "assistant",
provider: params.provider ?? "openai",

View File

@@ -3,7 +3,7 @@ import { normalizeUsage } from "../agents/usage.js";
import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import {
listSqliteSessionTranscriptFiles,
listSqliteSessionTranscriptLocators,
listSqliteSessionTranscripts,
loadSqliteSessionTranscriptEvents,
resolveSqliteSessionTranscriptScopeForPath,
@@ -263,7 +263,7 @@ const applyCostTotal = (totals: CostUsageTotals, costTotal: number | undefined)
};
function getRememberedTranscriptPath(agentId: string, sessionId: string): string | undefined {
return listSqliteSessionTranscriptFiles().find(
return listSqliteSessionTranscriptLocators().find(
(entry) => entry.agentId === agentId && entry.sessionId === sessionId,
)?.path;
}

View File

@@ -329,14 +329,6 @@ export interface TaskRuns {
terminal_summary: string | null;
}
export interface TranscriptFiles {
agent_id: string;
exported_at: number | null;
imported_at: number | null;
path: string;
session_id: string;
}
export interface TuiLastSessions {
scope_key: string;
session_key: string;
@@ -368,6 +360,5 @@ export interface DB {
subagent_runs: SubagentRuns;
task_delivery_state: TaskDeliveryState;
task_runs: TaskRuns;
transcript_files: TranscriptFiles;
tui_last_sessions: TuiLastSessions;
}

View File

@@ -188,22 +188,6 @@ CREATE INDEX IF NOT EXISTS idx_commitments_scope_due
CREATE INDEX IF NOT EXISTS idx_commitments_status_due
ON commitments(status, due_earliest_ms, due_latest_ms);
CREATE TABLE IF NOT EXISTS transcript_files (
agent_id TEXT NOT NULL,
session_id TEXT NOT NULL,
path TEXT NOT NULL,
imported_at INTEGER,
exported_at INTEGER,
PRIMARY KEY (agent_id, session_id, path),
FOREIGN KEY (agent_id) REFERENCES agent_databases(agent_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_transcript_files_path_updated
ON transcript_files(path, imported_at DESC, exported_at DESC, agent_id, session_id);
CREATE INDEX IF NOT EXISTS idx_transcript_files_session_updated
ON transcript_files(agent_id, session_id, imported_at DESC, exported_at DESC, path);
CREATE TABLE IF NOT EXISTS cron_run_logs (
store_key TEXT NOT NULL,
job_id TEXT NOT NULL,

View File

@@ -183,22 +183,6 @@ CREATE INDEX IF NOT EXISTS idx_commitments_scope_due
CREATE INDEX IF NOT EXISTS idx_commitments_status_due
ON commitments(status, due_earliest_ms, due_latest_ms);
CREATE TABLE IF NOT EXISTS transcript_files (
agent_id TEXT NOT NULL,
session_id TEXT NOT NULL,
path TEXT NOT NULL,
imported_at INTEGER,
exported_at INTEGER,
PRIMARY KEY (agent_id, session_id, path),
FOREIGN KEY (agent_id) REFERENCES agent_databases(agent_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_transcript_files_path_updated
ON transcript_files(path, imported_at DESC, exported_at DESC, agent_id, session_id);
CREATE INDEX IF NOT EXISTS idx_transcript_files_session_updated
ON transcript_files(agent_id, session_id, imported_at DESC, exported_at DESC, path);
CREATE TABLE IF NOT EXISTS cron_run_logs (
store_key TEXT NOT NULL,
job_id TEXT NOT NULL,