mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-10 20:45:15 +00:00
refactor: remove transcript locator cleanup leftovers
This commit is contained in:
@@ -257,7 +257,7 @@ The remaining cleanup is mostly consolidation and deletion:
|
||||
old `resolve...ForPath` helper and unused `transcriptPath` write options are
|
||||
gone from runtime callers.
|
||||
- Runtime session resolution now uses `{agentId, sessionId}` and must not derive
|
||||
`sqlite-transcript://<agent>/<session>` handles for external boundaries.
|
||||
`sqlite-transcript://<agent>/<session>` strings for external boundaries.
|
||||
Legacy absolute JSONL paths are doctor migration inputs only.
|
||||
- `runEmbeddedPiAgent(...)` no longer has a transcript-locator parameter.
|
||||
Prepared worker descriptors also omit transcript locators. Runtime session
|
||||
@@ -522,11 +522,10 @@ sessionId}` and session key context.
|
||||
SQLite reads. Its helper no longer accepts or derives transcript locators,
|
||||
legacy file reads, or file-rewrite options.
|
||||
- Codex app-server conversation bindings now key SQLite plugin state by
|
||||
OpenClaw session key when available, with transcript-path lookups kept only as
|
||||
a legacy fallback for existing bindings.
|
||||
- Codex app-server mirrored-history reads now prefer the SQLite transcript scope
|
||||
registered for the transcript path, falling back to `{agentId, sessionId}`
|
||||
only when the path has not been imported or mapped yet.
|
||||
OpenClaw session key or explicit `{agentId, sessionId}` scope. They must not
|
||||
preserve transcript-path fallback bindings.
|
||||
- Codex app-server mirrored-history reads use the SQLite transcript scope only;
|
||||
they must not recover identity from transcript file paths.
|
||||
- Role-ordering and compaction reset paths no longer unlink old transcript
|
||||
files; reset only rotates the SQLite session row and transcript identity.
|
||||
- Gateway reset and checkpoint responses return clean session rows plus session
|
||||
@@ -534,8 +533,8 @@ sessionId}` and session key context.
|
||||
- Memory-core dreaming no longer prunes session rows by probing for missing
|
||||
JSONL files. Subagent cleanup goes through the session runtime API instead of
|
||||
filesystem existence checks. Its transcript-ingestion tests seed SQLite rows
|
||||
through neutral test locators instead of creating `agents/<id>/sessions`
|
||||
fixtures.
|
||||
directly instead of creating `agents/<id>/sessions` fixtures or locator
|
||||
placeholders.
|
||||
- Gateway doctor memory status reads short-term recall and phase-signal counts
|
||||
from SQLite plugin-state rows instead of `memory/.dreams/*.json`; CLI and
|
||||
doctor output now label that storage as a SQLite store, not a path.
|
||||
|
||||
@@ -108,9 +108,7 @@ export async function handleClickClackInbound(params: {
|
||||
}
|
||||
const senderName = message.author?.display_name || message.author_id;
|
||||
const previousTimestamp = runtime.channel.session.readSessionUpdatedAt({
|
||||
storePath: runtime.channel.session.resolveStorePath(params.config.session?.store, {
|
||||
agentId: route.agentId,
|
||||
}),
|
||||
agentId: route.agentId,
|
||||
sessionKey: route.sessionKey,
|
||||
});
|
||||
const body = runtime.channel.reply.formatAgentEnvelope({
|
||||
@@ -121,9 +119,6 @@ export async function handleClickClackInbound(params: {
|
||||
envelope: runtime.channel.reply.resolveEnvelopeFormatOptions(params.config as OpenClawConfig),
|
||||
body: message.body,
|
||||
});
|
||||
const storePath = runtime.channel.session.resolveStorePath(params.config.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
||||
Body: body,
|
||||
BodyForAgent: message.body,
|
||||
@@ -161,8 +156,8 @@ export async function handleClickClackInbound(params: {
|
||||
await runtime.channel.turn.runPrepared({
|
||||
channel: CHANNEL_ID,
|
||||
accountId: params.account.accountId,
|
||||
agentId: route.agentId,
|
||||
routeSessionKey: route.sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: runtime.channel.session.recordInboundSession,
|
||||
runDispatch: async () =>
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import {
|
||||
buildSessionTranscriptEntry,
|
||||
listSessionTranscriptsForAgent,
|
||||
sessionPathForTranscript,
|
||||
sessionSourceKeyForTranscript,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-qmd";
|
||||
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
|
||||
import {
|
||||
@@ -628,17 +628,17 @@ function areStringArraysEqual(a: string[], b: string[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildSessionStateKey(agentId: string, sessionPath: string): string {
|
||||
return `${agentId}:${sessionPath}`;
|
||||
function buildSessionStateKey(agentId: string, sessionSourceKey: string): string {
|
||||
return `${agentId}:${sessionSourceKey}`;
|
||||
}
|
||||
|
||||
function buildSessionRenderedLine(params: {
|
||||
agentId: string;
|
||||
sessionPath: string;
|
||||
sessionSourceKey: string;
|
||||
lineNumber: number;
|
||||
snippet: string;
|
||||
}): string {
|
||||
const source = `${params.agentId}/${params.sessionPath}#L${params.lineNumber}`;
|
||||
const source = `${params.agentId}/${params.sessionSourceKey}#L${params.lineNumber}`;
|
||||
return `[${source}] ${params.snippet}`.slice(0, SESSION_INGESTION_MAX_SNIPPET_CHARS + 64);
|
||||
}
|
||||
|
||||
@@ -727,7 +727,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
const sessionTranscripts: Array<{
|
||||
agentId: string;
|
||||
scope: { agentId: string; sessionId: string };
|
||||
sessionPath: string;
|
||||
sessionSourceKey: string;
|
||||
}> = [];
|
||||
for (const agentId of agentIds) {
|
||||
const scopes = await listSessionTranscriptsForAgent(agentId);
|
||||
@@ -735,7 +735,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
sessionTranscripts.push({
|
||||
agentId,
|
||||
scope,
|
||||
sessionPath: sessionPathForTranscript(scope),
|
||||
sessionSourceKey: sessionSourceKeyForTranscript(scope),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -744,7 +744,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
if (a.agentId !== b.agentId) {
|
||||
return a.agentId.localeCompare(b.agentId);
|
||||
}
|
||||
return a.sessionPath.localeCompare(b.sessionPath);
|
||||
return a.sessionSourceKey.localeCompare(b.sessionSourceKey);
|
||||
});
|
||||
|
||||
const totalCap = SESSION_INGESTION_MAX_MESSAGES_PER_SWEEP;
|
||||
@@ -761,7 +761,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
const stateKey = buildSessionStateKey(file.agentId, file.sessionPath);
|
||||
const stateKey = buildSessionStateKey(file.agentId, file.sessionSourceKey);
|
||||
const previous = params.state.files[stateKey];
|
||||
const entry = await buildSessionTranscriptEntry(file.scope);
|
||||
if (!entry) {
|
||||
@@ -819,7 +819,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sessionScope = buildSessionScopeKey(file.agentId, file.sessionPath);
|
||||
const sessionScope = buildSessionScopeKey(file.agentId, file.sessionSourceKey);
|
||||
const previousSeen = nextSeenMessages[sessionScope] ?? [];
|
||||
let seenSet = new Set(previousSeen);
|
||||
const newSeenHashes: string[] = [];
|
||||
@@ -865,7 +865,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
}
|
||||
const rendered = buildSessionRenderedLine({
|
||||
agentId: file.agentId,
|
||||
sessionPath: file.sessionPath,
|
||||
sessionSourceKey: file.sessionSourceKey,
|
||||
lineNumber,
|
||||
snippet,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { resolveMemorySessionSyncPlan } from "./manager-session-sync-state.js";
|
||||
|
||||
describe("memory session sync state", () => {
|
||||
it("tracks active paths and bulk hashes for full scans", () => {
|
||||
it("tracks active source keys and bulk hashes for full scans", () => {
|
||||
const plan = resolveMemorySessionSyncPlan({
|
||||
needsFullReindex: false,
|
||||
files: [
|
||||
@@ -12,22 +12,22 @@ describe("memory session sync state", () => {
|
||||
targetSessionTranscriptKeys: null,
|
||||
dirtySessionTranscripts: new Set(),
|
||||
existingRows: [
|
||||
{ path: "sessions/a.jsonl", hash: "hash-a" },
|
||||
{ path: "sessions/b.jsonl", hash: "hash-b" },
|
||||
{ path: "sessions/main/a", hash: "hash-a" },
|
||||
{ path: "sessions/main/b", hash: "hash-b" },
|
||||
],
|
||||
sessionPathForTranscript: (scope) => `sessions/${scope.sessionId}.jsonl`,
|
||||
sessionSourceKeyForTranscript: (scope) => `sessions/${scope.agentId}/${scope.sessionId}`,
|
||||
});
|
||||
|
||||
expect(plan.indexAll).toBe(true);
|
||||
expect(plan.activePaths).toEqual(new Set(["sessions/a.jsonl", "sessions/b.jsonl"]));
|
||||
expect(plan.activePaths).toEqual(new Set(["sessions/main/a", "sessions/main/b"]));
|
||||
expect(plan.existingRows).toEqual([
|
||||
{ path: "sessions/a.jsonl", hash: "hash-a" },
|
||||
{ path: "sessions/b.jsonl", hash: "hash-b" },
|
||||
{ path: "sessions/main/a", hash: "hash-a" },
|
||||
{ path: "sessions/main/b", hash: "hash-b" },
|
||||
]);
|
||||
expect(plan.existingHashes).toEqual(
|
||||
new Map([
|
||||
["sessions/a.jsonl", "hash-a"],
|
||||
["sessions/b.jsonl", "hash-b"],
|
||||
["sessions/main/a", "hash-a"],
|
||||
["sessions/main/b", "hash-b"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -39,10 +39,10 @@ describe("memory session sync state", () => {
|
||||
targetSessionTranscriptKeys: new Set(["main\0targeted-first"]),
|
||||
dirtySessionTranscripts: new Set(["main\0targeted-first"]),
|
||||
existingRows: [
|
||||
{ path: "sessions/targeted-first.jsonl", hash: "hash-first" },
|
||||
{ path: "sessions/targeted-second.jsonl", hash: "hash-second" },
|
||||
{ path: "sessions/main/targeted-first", hash: "hash-first" },
|
||||
{ path: "sessions/main/targeted-second", hash: "hash-second" },
|
||||
],
|
||||
sessionPathForTranscript: (scope) => `sessions/${scope.sessionId}.jsonl`,
|
||||
sessionSourceKeyForTranscript: (scope) => `sessions/${scope.agentId}/${scope.sessionId}`,
|
||||
});
|
||||
|
||||
expect(plan.indexAll).toBe(true);
|
||||
@@ -58,10 +58,10 @@ describe("memory session sync state", () => {
|
||||
targetSessionTranscriptKeys: null,
|
||||
dirtySessionTranscripts: new Set(["main\0incremental"]),
|
||||
existingRows: [],
|
||||
sessionPathForTranscript: (scope) => `sessions/${scope.sessionId}.jsonl`,
|
||||
sessionSourceKeyForTranscript: (scope) => `sessions/${scope.agentId}/${scope.sessionId}`,
|
||||
});
|
||||
|
||||
expect(plan.indexAll).toBe(false);
|
||||
expect(plan.activePaths).toEqual(new Set(["sessions/incremental.jsonl"]));
|
||||
expect(plan.activePaths).toEqual(new Set(["sessions/main/incremental"]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export function resolveMemorySessionSyncPlan(params: {
|
||||
targetSessionTranscriptKeys: Set<string> | null;
|
||||
dirtySessionTranscripts: Set<string>;
|
||||
existingRows?: MemorySourceFileStateRow[] | null;
|
||||
sessionPathForTranscript: (scope: MemorySessionSyncScope) => string;
|
||||
sessionSourceKeyForTranscript: (scope: MemorySessionSyncScope) => string;
|
||||
}): {
|
||||
activePaths: Set<string> | null;
|
||||
existingRows: MemorySourceFileStateRow[] | null;
|
||||
@@ -20,7 +20,7 @@ export function resolveMemorySessionSyncPlan(params: {
|
||||
} {
|
||||
const activePaths = params.targetSessionTranscriptKeys
|
||||
? null
|
||||
: new Set(params.files.map((file) => params.sessionPathForTranscript(file)));
|
||||
: new Set(params.files.map((file) => params.sessionSourceKeyForTranscript(file)));
|
||||
const existingRows = activePaths === null ? null : (params.existingRows ?? []);
|
||||
return {
|
||||
activePaths,
|
||||
|
||||
@@ -51,8 +51,8 @@ describe("memory source state", () => {
|
||||
}),
|
||||
},
|
||||
source: "sessions",
|
||||
path: "sessions/thread.jsonl",
|
||||
existingHashes: new Map([["sessions/thread.jsonl", "hash-from-snapshot"]]),
|
||||
path: "sessions/main/thread",
|
||||
existingHashes: new Map([["sessions/main/thread", "hash-from-snapshot"]]),
|
||||
});
|
||||
|
||||
expect(hash).toBe("hash-from-snapshot");
|
||||
@@ -72,7 +72,7 @@ describe("memory source state", () => {
|
||||
}),
|
||||
},
|
||||
source: "sessions",
|
||||
path: "sessions/thread.jsonl",
|
||||
path: "sessions/main/thread",
|
||||
existingHashes: null,
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("memory source state", () => {
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
sql: MEMORY_SOURCE_FILE_HASH_SQL,
|
||||
args: ["sessions/thread.jsonl", "sessions"],
|
||||
args: ["sessions/main/thread", "sessions"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
buildSessionTranscriptEntry,
|
||||
listSessionTranscriptsForAgent,
|
||||
readSessionTranscriptDeltaStats,
|
||||
sessionPathForTranscript,
|
||||
sessionSourceKeyForTranscript,
|
||||
type SessionTranscriptScope,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-qmd";
|
||||
import {
|
||||
@@ -804,7 +804,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
db: this.db,
|
||||
source: "sessions",
|
||||
}).rows,
|
||||
sessionPathForTranscript,
|
||||
sessionSourceKeyForTranscript,
|
||||
});
|
||||
const { activePaths, existingRows, existingHashes, indexAll } = sessionPlan;
|
||||
log.debug("memory sync: indexing session transcripts", {
|
||||
|
||||
@@ -200,7 +200,7 @@ describe("QmdMemoryManager", () => {
|
||||
return { manager: requireValue(manager, "manager missing"), resolved };
|
||||
}
|
||||
|
||||
function seedSessionTranscript(params?: { sessionId?: string; content?: string }): string {
|
||||
function seedSessionTranscript(params?: { sessionId?: string; content?: string }): void {
|
||||
const sessionId = params?.sessionId ?? "session-1";
|
||||
replaceSqliteSessionTranscriptEvents({
|
||||
agentId,
|
||||
@@ -212,7 +212,6 @@ describe("QmdMemoryManager", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
return `sqlite-transcript://${encodeURIComponent(agentId)}/${encodeURIComponent(sessionId)}.jsonl`;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -8,7 +8,6 @@ const crossAgentStore = {
|
||||
"agent:peer:only": {
|
||||
sessionId: "w1",
|
||||
updatedAt: 1,
|
||||
sessionTranscript: "/tmp/sessions/w1.jsonl",
|
||||
},
|
||||
};
|
||||
let combinedSessionEntries: typeof crossAgentStore | Record<string, never> = crossAgentStore;
|
||||
@@ -35,7 +34,7 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
|
||||
const cfg = asOpenClawConfig({ tools: { sessions: { visibility: "all" } } });
|
||||
const hits: MemorySearchResult[] = [
|
||||
{
|
||||
path: "sessions/u1.jsonl",
|
||||
path: "sessions/main/u1",
|
||||
source: "sessions",
|
||||
score: 1,
|
||||
snippet: "x",
|
||||
@@ -77,7 +76,7 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
|
||||
const cfg = asOpenClawConfig({ tools: { sessions: { visibility: "all" } } });
|
||||
const hits: MemorySearchResult[] = [
|
||||
{
|
||||
path: "sessions/w1.jsonl",
|
||||
path: "sessions/peer/w1",
|
||||
source: "sessions",
|
||||
score: 1,
|
||||
snippet: "a",
|
||||
@@ -85,7 +84,7 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
|
||||
endLine: 2,
|
||||
},
|
||||
{
|
||||
path: "sessions/w1.jsonl",
|
||||
path: "sessions/peer/w1",
|
||||
source: "sessions",
|
||||
score: 0.9,
|
||||
snippet: "b",
|
||||
@@ -105,7 +104,7 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
|
||||
|
||||
it("allows cross-agent session hits when visibility=all and agent-to-agent is enabled", async () => {
|
||||
const hit: MemorySearchResult = {
|
||||
path: "sessions/w1.jsonl",
|
||||
path: "sessions/peer/w1",
|
||||
source: "sessions",
|
||||
score: 1,
|
||||
snippet: "x",
|
||||
@@ -129,7 +128,7 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
|
||||
|
||||
it("denies cross-agent session hits when agent-to-agent is disabled", async () => {
|
||||
const hit: MemorySearchResult = {
|
||||
path: "sessions/w1.jsonl",
|
||||
path: "sessions/peer/w1",
|
||||
source: "sessions",
|
||||
score: 1,
|
||||
snippet: "x",
|
||||
|
||||
@@ -5,7 +5,7 @@ export {
|
||||
buildSessionTranscriptEntry,
|
||||
listSessionTranscriptsForAgent,
|
||||
readSessionTranscriptDeltaStats,
|
||||
sessionPathForTranscript,
|
||||
sessionSourceKeyForTranscript,
|
||||
type BuildSessionTranscriptEntryOptions,
|
||||
type SessionTranscriptEntry,
|
||||
type SessionTranscriptDeltaStats,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
buildSessionTranscriptEntry,
|
||||
listSessionTranscriptsForAgent,
|
||||
readSessionTranscriptDeltaStats,
|
||||
sessionPathForTranscript,
|
||||
sessionSourceKeyForTranscript,
|
||||
type SessionTranscriptEntry,
|
||||
type SessionTranscriptScope,
|
||||
} from "./session-transcripts.js";
|
||||
@@ -103,7 +103,7 @@ describe("listSessionTranscriptsForAgent", () => {
|
||||
expect(scopes).toEqual([scope]);
|
||||
const entry = await buildSessionTranscriptEntry(scope);
|
||||
expect(entry?.content).toBe("User: Stored only in SQLite");
|
||||
expect(entry?.path).toBe("sessions/main/sqlite-only.jsonl");
|
||||
expect(entry?.path).toBe("sessions/main/sqlite-only");
|
||||
});
|
||||
|
||||
it("ignores remembered legacy transcript paths when listing active SQLite transcripts", async () => {
|
||||
@@ -120,10 +120,10 @@ describe("listSessionTranscriptsForAgent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionPathForTranscript", () => {
|
||||
it("formats SQLite scopes as stable session export paths", () => {
|
||||
expect(sessionPathForTranscript({ agentId: "main", sessionId: "active-session" })).toBe(
|
||||
"sessions/main/active-session.jsonl",
|
||||
describe("sessionSourceKeyForTranscript", () => {
|
||||
it("formats SQLite scopes as stable memory source keys", () => {
|
||||
expect(sessionSourceKeyForTranscript({ agentId: "main", sessionId: "active-session" })).toBe(
|
||||
"sessions/main/active-session",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,8 +153,8 @@ export async function listSessionTranscriptsForAgent(
|
||||
}));
|
||||
}
|
||||
|
||||
export function sessionPathForTranscript(scope: SessionTranscriptScope): string {
|
||||
return `sessions/${scope.agentId}/${scope.sessionId}.jsonl`;
|
||||
export function sessionSourceKeyForTranscript(scope: SessionTranscriptScope): string {
|
||||
return `sessions/${scope.agentId}/${scope.sessionId}`;
|
||||
}
|
||||
|
||||
export function readSessionTranscriptDeltaStats(
|
||||
@@ -459,7 +459,7 @@ export async function buildSessionTranscriptEntry(
|
||||
const content = collected.join("\n");
|
||||
return {
|
||||
scope,
|
||||
path: sessionPathForTranscript(scope),
|
||||
path: sessionSourceKeyForTranscript(scope),
|
||||
mtimeMs,
|
||||
size,
|
||||
messageCount,
|
||||
|
||||
@@ -187,7 +187,8 @@ vi.mock("../config/sessions.js", () => ({
|
||||
|
||||
vi.mock("../config/sessions/transcript-resolve.runtime.js", () => ({
|
||||
resolveSessionTranscriptTarget: async () => ({
|
||||
sessionFile: "sqlite-transcript://default/session-1.jsonl",
|
||||
agentId: "default",
|
||||
sessionId: "session-1",
|
||||
sessionEntry: { sessionId: "session-1", updatedAt: Date.now() },
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1051,7 +1051,9 @@ async function agentCommandInternal(
|
||||
agentId: sessionAgentId,
|
||||
sessionId,
|
||||
})),
|
||||
suppressPromptPersistenceOnRetry: isFallbackRetry && currentTurnUserMessagePersisted,
|
||||
suppressPromptPersistenceOnRetry:
|
||||
opts.suppressPromptPersistence === true ||
|
||||
(isFallbackRetry && currentTurnUserMessagePersisted),
|
||||
onUserMessagePersisted: () => {
|
||||
currentTurnUserMessagePersisted = true;
|
||||
},
|
||||
|
||||
@@ -61,7 +61,6 @@ const baseRunParams = {
|
||||
sessionId: "test-session",
|
||||
sessionKey: "test-session-key",
|
||||
agentId: "main",
|
||||
sessionFile: "sqlite-transcript://main/test-session.jsonl",
|
||||
workspaceDir: "/tmp/test-workspace",
|
||||
prompt: "__openclaw_memory_core_short_term_promotion_dream__",
|
||||
provider: "codex-cli",
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("overflow compaction in run loop", () => {
|
||||
expect.objectContaining({ contextWindowTokens: 200000 }),
|
||||
);
|
||||
expect(mockedTruncateOversizedToolResultsInSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionFile: "sqlite-transcript://main/test-session.jsonl" }),
|
||||
expect.objectContaining({ agentId: "main", sessionId: "test-session" }),
|
||||
);
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
expect(mockedLog.info).toHaveBeenCalledWith(
|
||||
@@ -302,7 +302,7 @@ describe("overflow compaction in run loop", () => {
|
||||
}),
|
||||
);
|
||||
expect(mockedTruncateOversizedToolResultsInSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionFile: "sqlite-transcript://main/test-session.jsonl" }),
|
||||
expect.objectContaining({ agentId: "main", sessionId: "test-session" }),
|
||||
);
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
expect(mockedLog.info).toHaveBeenCalledWith(
|
||||
@@ -472,7 +472,7 @@ describe("overflow compaction in run loop", () => {
|
||||
|
||||
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedTruncateOversizedToolResultsInSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionFile: "sqlite-transcript://main/test-session.jsonl" }),
|
||||
expect.objectContaining({ agentId: "main", sessionId: "test-session" }),
|
||||
);
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
expect(mockedLog.info).toHaveBeenCalledWith(
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("timeout-triggered compaction", () => {
|
||||
expect(mockedCompactDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: "test-session",
|
||||
transcriptLocator: "sqlite-transcript://main/test-session",
|
||||
transcriptScope: { agentId: "main", sessionId: "test-session" },
|
||||
tokenBudget: 200000,
|
||||
force: true,
|
||||
compactionTarget: "budget",
|
||||
|
||||
@@ -2550,6 +2550,8 @@ export async function runEmbeddedPiAgent(
|
||||
// partial assistant fragment. Emit an explicit timeout error instead.
|
||||
if (
|
||||
timedOutDuringPrompt &&
|
||||
!attempt.didSendViaMessagingTool &&
|
||||
!attempt.didSendDeterministicApprovalPrompt &&
|
||||
(!payloadsWithToolMedia?.length || hasPartialAssistantTextAfterPromptTimeout)
|
||||
) {
|
||||
const timeoutText = idleTimedOut
|
||||
|
||||
@@ -164,7 +164,9 @@ export async function loadSubagentSpawnModuleForTest(params: {
|
||||
buildSubagentSystemPrompt: () => "system-prompt",
|
||||
forkSessionFromParent:
|
||||
params.forkSessionFromParentMock ??
|
||||
(async () => ({ sessionId: "forked-session-id", sessionFile: "/tmp/forked-session.jsonl" })),
|
||||
(async () => ({
|
||||
sessionId: "forked-session-id",
|
||||
})),
|
||||
getGlobalHookRunner: () => params.hookRunner ?? { hasHooks: () => false },
|
||||
emitSessionLifecycleEvent: (...args: unknown[]) =>
|
||||
params.emitSessionLifecycleEventMock?.(...args),
|
||||
|
||||
@@ -7,7 +7,7 @@ const runtime = vi.hoisted(() => ({
|
||||
resolveSessionAgentId: vi.fn(() => "main"),
|
||||
loadSessionEntry: vi.fn(() => ({
|
||||
cfg: {},
|
||||
entry: { sessionId: "sess-main", sessionFile: "sqlite-transcript://main/sess-main.jsonl" },
|
||||
entry: { sessionId: "sess-main" },
|
||||
})),
|
||||
resolveSessionModelRef: vi.fn(() => ({ provider: "openai" })),
|
||||
readSessionMessagesAsync: vi.fn(async (): Promise<unknown[]> => []),
|
||||
@@ -91,8 +91,10 @@ describe("embedded gateway stub", () => {
|
||||
maxMessages: 200,
|
||||
});
|
||||
expect(runtime.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"sqlite-transcript://main/sess-main.jsonl",
|
||||
{
|
||||
agentId: "main",
|
||||
sessionId: "sess-main",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 200,
|
||||
@@ -120,8 +122,10 @@ describe("embedded gateway stub", () => {
|
||||
maxMessages: 1,
|
||||
});
|
||||
expect(runtime.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"sqlite-transcript://main/sess-main.jsonl",
|
||||
{
|
||||
agentId: "main",
|
||||
sessionId: "sess-main",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 1,
|
||||
|
||||
@@ -9,10 +9,6 @@ import type { HandleCommandsParams } from "./commands-types.js";
|
||||
vi.mock("./commands-compact.runtime.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn(),
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
createSqliteSessionTranscriptLocator: vi.fn(
|
||||
({ agentId, sessionId }: { agentId: string; sessionId: string }) =>
|
||||
`sqlite-transcript://${agentId}/${sessionId}`,
|
||||
),
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
formatContextUsageShort: vi.fn(() => "Context 12.1k"),
|
||||
formatTokenCount: vi.fn((value: number) => `${value}`),
|
||||
|
||||
@@ -24,13 +24,6 @@ vi.mock("../../config/sessions/group.js", () => ({
|
||||
resolveGroupSessionKey: vi.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/sessions/test-helpers/transcript-locator.js", () => ({
|
||||
createSqliteSessionTranscriptLocator: vi.fn(
|
||||
({ agentId, sessionId }: { agentId?: string; sessionId: string }) =>
|
||||
`sqlite-transcript://${agentId ?? "main"}/${sessionId}`,
|
||||
),
|
||||
}));
|
||||
|
||||
const storeRuntimeLoads = vi.hoisted(() => vi.fn());
|
||||
const upsertSessionEntry = vi.hoisted(() => vi.fn());
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type SessionFreshness,
|
||||
} from "../../config/sessions/reset.js";
|
||||
import { resolveSessionKey } from "../../config/sessions/session-key.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "../../config/sessions/session-locator.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "../../config/sessions/session-scope.js";
|
||||
import {
|
||||
getSessionEntry,
|
||||
listSessionEntries,
|
||||
|
||||
@@ -10,7 +10,7 @@ export * from "./sessions/session-key.js";
|
||||
export * from "./sessions/store.js";
|
||||
export * from "./sessions/types.js";
|
||||
export * from "./sessions/transcript.js";
|
||||
export * from "./sessions/session-locator.js";
|
||||
export * from "./sessions/session-scope.js";
|
||||
export * from "./sessions/delivery-info.js";
|
||||
export * from "./sessions/targets.js";
|
||||
export * from "./sessions/agent-purge.js";
|
||||
|
||||
39
src/config/sessions/session-scope.ts
Normal file
39
src/config/sessions/session-scope.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { getSessionEntry, upsertSessionEntry } from "./store.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
export async function resolveAndPersistSessionTranscriptScope(params: {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentId?: string;
|
||||
topicId?: string | number;
|
||||
}): Promise<{ agentId: string; sessionId: string; sessionEntry: SessionEntry }> {
|
||||
const { sessionId, sessionKey } = params;
|
||||
const now = Date.now();
|
||||
const agentId = params.agentId ?? resolveAgentIdFromSessionKey(sessionKey);
|
||||
if (!agentId) {
|
||||
throw new Error(`Session stores are SQLite-only; cannot resolve agent for ${sessionKey}`);
|
||||
}
|
||||
const baseEntry = params.sessionEntry ??
|
||||
getSessionEntry({ agentId, sessionKey }) ?? {
|
||||
sessionId,
|
||||
updatedAt: now,
|
||||
sessionStartedAt: now,
|
||||
};
|
||||
const persistedEntry: SessionEntry = {
|
||||
...baseEntry,
|
||||
sessionId,
|
||||
updatedAt: now,
|
||||
sessionStartedAt: baseEntry.sessionId === sessionId ? (baseEntry.sessionStartedAt ?? now) : now,
|
||||
};
|
||||
if (baseEntry.sessionId !== sessionId) {
|
||||
upsertSessionEntry({
|
||||
agentId,
|
||||
sessionKey,
|
||||
entry: persistedEntry,
|
||||
});
|
||||
return { agentId, sessionId, sessionEntry: persistedEntry };
|
||||
}
|
||||
return { agentId, sessionId, sessionEntry: persistedEntry };
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { SessionConfig } from "../types.base.js";
|
||||
import { resolveSessionLifecycleTimestamps } from "./lifecycle.js";
|
||||
import { validateSessionId } from "./paths.js";
|
||||
import { evaluateSessionFreshness, resolveSessionResetPolicy } from "./reset.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "./session-locator.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "./session-scope.js";
|
||||
import {
|
||||
getSessionEntry,
|
||||
listSessionEntries,
|
||||
@@ -16,10 +16,6 @@ import {
|
||||
upsertSessionEntry,
|
||||
} from "./store.js";
|
||||
import { useTempSessionsFixture } from "./test-helpers.js";
|
||||
import {
|
||||
createSqliteSessionTranscriptLocator,
|
||||
resolveSessionTranscriptLocator,
|
||||
} from "./test-helpers/transcript-locator.js";
|
||||
import { replaceSqliteSessionTranscriptEvents } from "./transcript-store.sqlite.js";
|
||||
import { mergeSessionEntry, mergeSessionEntryWithPolicy, type SessionEntry } from "./types.js";
|
||||
|
||||
@@ -36,17 +32,6 @@ describe("session path safety", () => {
|
||||
expect(() => validateSessionId(sessionId), sessionId).toThrow(/Invalid session ID/);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores invalid transcript locators", () => {
|
||||
const resolved = resolveSessionTranscriptLocator("sess-1");
|
||||
expect(resolved).toBe(createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }));
|
||||
});
|
||||
|
||||
it("uses extensionless SQLite transcript locators by default", () => {
|
||||
expect(resolveSessionTranscriptLocator("sess-1")).toBe(
|
||||
createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionResetPolicy", () => {
|
||||
@@ -188,10 +173,6 @@ describe("session lifecycle timestamps", () => {
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
process.env.OPENCLAW_STATE_DIR = dir;
|
||||
try {
|
||||
const transcriptLocator = createSqliteSessionTranscriptLocator({
|
||||
agentId: "main",
|
||||
sessionId: "lifecycle-session",
|
||||
});
|
||||
const headerTimestamp = "2026-04-20T04:30:00.000Z";
|
||||
replaceSqliteSessionTranscriptEvents({
|
||||
agentId: "main",
|
||||
@@ -474,7 +455,7 @@ describe("SQLite session store patch retries", () => {
|
||||
});
|
||||
|
||||
describe("resolveAndPersistSessionTranscriptScope", () => {
|
||||
const fixture = useTempSessionsFixture("session-locator-test-");
|
||||
const fixture = useTempSessionsFixture("session-scope-test-");
|
||||
|
||||
function readFixtureSessionEntries(): Record<string, SessionEntry> {
|
||||
return Object.fromEntries(
|
||||
|
||||
@@ -168,11 +168,9 @@ describe("SQLite session transcript store", () => {
|
||||
).toEqual([{ type: "message", id: "main" }]);
|
||||
});
|
||||
|
||||
it("lists SQLite transcripts with canonical transcript locators", () => {
|
||||
it("lists SQLite transcript scopes", () => {
|
||||
const stateDir = createTempDir();
|
||||
const env = { OPENCLAW_STATE_DIR: stateDir };
|
||||
const olderPath = path.join(stateDir, "session-old.jsonl");
|
||||
const newerPath = path.join(stateDir, "session-new.jsonl");
|
||||
|
||||
appendSqliteSessionTranscriptEvent({
|
||||
env,
|
||||
@@ -202,7 +200,6 @@ describe("SQLite session transcript store", () => {
|
||||
it("deletes transcript snapshots with the transcript", () => {
|
||||
const stateDir = createTempDir();
|
||||
const env = { OPENCLAW_STATE_DIR: stateDir };
|
||||
const transcriptPath = path.join(stateDir, "session.jsonl");
|
||||
|
||||
appendSqliteSessionTranscriptEvent({
|
||||
env,
|
||||
@@ -231,7 +228,6 @@ describe("SQLite session transcript store", () => {
|
||||
|
||||
it("renders JSONL from SQLite for explicit transcript export", () => {
|
||||
const stateDir = createTempDir();
|
||||
const sourcePath = path.join(stateDir, "source.jsonl");
|
||||
|
||||
replaceSqliteSessionTranscriptEvents({
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../routing/session-key.js";
|
||||
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import { extractAssistantVisibleText } from "../../shared/chat-message-content.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "./session-locator.js";
|
||||
import { resolveAndPersistSessionTranscriptScope } from "./session-scope.js";
|
||||
import { getSessionEntry, normalizeSessionRowKey } from "./store.js";
|
||||
import { parseSessionThreadInfo } from "./thread-info.js";
|
||||
import { appendSessionTranscriptMessage } from "./transcript-append.js";
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { createSqliteSessionTranscriptLocator } from "../config/sessions/test-helpers/transcript-locator.js";
|
||||
import { replaceSqliteSessionTranscriptEvents } from "../config/sessions/transcript-store.sqlite.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
|
||||
@@ -52,26 +51,19 @@ function setupState(prefix = "openclaw-session-utils-sqlite-") {
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
}
|
||||
|
||||
function transcriptPath(sessionId: string, agentId = "main"): string {
|
||||
return createSqliteSessionTranscriptLocator({ agentId, sessionId });
|
||||
}
|
||||
|
||||
function seedTranscript(params: {
|
||||
sessionId: string;
|
||||
agentId?: string;
|
||||
events: TranscriptEvent[];
|
||||
filePath?: string;
|
||||
}) {
|
||||
setupStateIfNeeded();
|
||||
const agentId = params.agentId ?? "main";
|
||||
const filePath = params.filePath ?? transcriptPath(params.sessionId, agentId);
|
||||
replaceSqliteSessionTranscriptEvents({
|
||||
agentId,
|
||||
sessionId: params.sessionId,
|
||||
events: params.events,
|
||||
now: () => 1_778_100_000_000,
|
||||
});
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function setupStateIfNeeded() {
|
||||
@@ -309,15 +301,12 @@ describe("SQLite transcript readers", () => {
|
||||
test("requires explicit SQLite transcript scope", () => {
|
||||
setupState();
|
||||
const sessionId = "cross-agent";
|
||||
const filePath = transcriptPath(sessionId, "ops");
|
||||
seedTranscript({
|
||||
agentId: "ops",
|
||||
sessionId,
|
||||
filePath,
|
||||
events: [header(sessionId), message("user", "from ops")],
|
||||
});
|
||||
|
||||
expect(filePath).toBe("sqlite-transcript://ops/cross-agent");
|
||||
expect(readSessionMessages({ sessionId })).toEqual([]);
|
||||
expect(readSessionMessages({ agentId: "ops", sessionId })).toEqual([
|
||||
expect.objectContaining({ content: "from ops" }),
|
||||
|
||||
@@ -6,7 +6,7 @@ export {
|
||||
} from "../state/openclaw-agent-db.js";
|
||||
export { openOpenClawStateDatabase } from "../state/openclaw-state-db.js";
|
||||
export { resolveSessionRowEntry } from "../config/sessions/store-entry.js";
|
||||
export { resolveAndPersistSessionTranscriptScope } from "../config/sessions/session-locator.js";
|
||||
export { resolveAndPersistSessionTranscriptScope } from "../config/sessions/session-scope.js";
|
||||
export { resolveSessionKey } from "../config/sessions/session-key.js";
|
||||
export { resolveGroupSessionKey } from "../config/sessions/group.js";
|
||||
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSqliteSessionTranscriptLocator } from "../config/sessions/test-helpers/transcript-locator.js";
|
||||
import {
|
||||
__setRealtimeVoiceAgentConsultDepsForTest,
|
||||
consultRealtimeVoiceAgent,
|
||||
@@ -14,7 +13,6 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) {
|
||||
{
|
||||
sessionId?: string;
|
||||
updatedAt?: number;
|
||||
transcriptLocator?: string;
|
||||
spawnedBy?: string;
|
||||
forkedFromParent?: boolean;
|
||||
totalTokens?: number;
|
||||
@@ -213,10 +211,6 @@ describe("realtime voice agent consult runtime", () => {
|
||||
const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime();
|
||||
sessionStore["agent:main:main"] = {
|
||||
sessionId: "parent-session",
|
||||
transcriptLocator: createSqliteSessionTranscriptLocator({
|
||||
agentId: "main",
|
||||
sessionId: "parent-session",
|
||||
}),
|
||||
totalTokens: 100,
|
||||
updatedAt: 1,
|
||||
};
|
||||
@@ -227,7 +221,6 @@ describe("realtime voice agent consult runtime", () => {
|
||||
}));
|
||||
const forkSessionFromParent = vi.fn(async () => ({
|
||||
sessionId: "forked-session",
|
||||
transcriptLocator: "sqlite-transcript://main/forked-session",
|
||||
}));
|
||||
__setRealtimeVoiceAgentConsultDepsForTest({
|
||||
resolveParentForkDecision,
|
||||
|
||||
Reference in New Issue
Block a user