mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
5621979a46
* feat(models): add session-only model selection * fix(models): use trailing session scope option * test(models): satisfy session scope lint * fix(models): reject duplicate model options * fix(models): clarify default and session scope * fix(models): require complete session option tokens * fix(models): report configured default dispatch * fix(models): keep directive handler within lint limit * fix(models): parse model options in either order * fix(models): apply session scope to aliases * fix(models): align alias scope with reply routing * fix(discord): surface model selection scope in picker * fix(models): preserve mixed-text model selection * fix(models): centralize command selection ownership * fix(models): align session scope lifecycle * fix(models): preserve command and auth ownership * fixup! fix(models): preserve command and auth ownership * fix(auth): preserve scoped CLI provider discovery * test(models): align result and cron fixtures * test(models): nest result timing metadata * fix(discord): narrow silent dispatch results * fix(transcript): preserve admitted turn identity * fix(context-engine): fence the admitted transcript turn * fix(context-engine): stabilize plugin compatibility contract * chore(plugin-sdk): refresh context engine API baseline * chore(plugin-sdk): use Linux context engine API baseline * fix(context-engine): align fallback ownership * fix(fallback): scope auth skip cache by profile * fix(context-engine): settle only accepted fallback turns * refactor(sessions): issue canonical turn admissions * refactor(context-engine): own logical turn advancement * fix(context-engine): settle cron fallback winners * fix(models): align picker and fallback transactions * fix(delivery): notify block admission after queueing * fix(sessions): preserve canonical admission receipts * chore(plugin-sdk): refresh API baseline hash * fix(context-engine): commit accepted turns durably * fix(context-engine): validate durable host transitions * fix(context-engine): preserve fallback turn ownership * fix(context-engine): preserve queued turn order * fix(models): preserve fallback retry ownership * fix(context-engine): enforce durable transcript anchors * fix(runtime): close fallback persistence gaps * fix(context-engine): preflight fallback harnesses * chore(plugin-sdk): use Linux API baseline * fix(context-engine): drain durable commits before reads * fix(models): scope harness auth failures by profile * fix(codex): fence legacy transcript history * fix(commands): honor suppressed directive interpretation * chore(runtime): remove unused branch exports * test(context-engine): derive private outbox payload type * fix(context-engine): apply durable drain degradation * fix(context-engine): recover durable turn intents * fix(context-engine): settle durable turn intents * refactor(context-engine): satisfy branch quality gates * fix(context-engine): close durable recovery gaps * fix(discord): preserve dropped model command outcome * test(copilot): keep journal fixture types local * fix(auto-reply): preserve model alias provenance * fix: close model scope review gaps * fix(models): close review-found scope leaks * fix(review): satisfy branch line budgets * fix(agents): preserve context engine turn facts * fix(agents): finalize silent context turns * fix(context-engine): preserve compatibility window * test(agents): cover both harness preparations * fix(context-engine): retain blocked turn advancements * fix(models): parse compact runtime options * fix(telegram): report runtime resets accurately * fix(models): isolate automatic auth failure skips * fix(context-engine): project commit turn host params --------- Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
/**
|
|
* Reads OpenClaw session history for Codex transcript mirroring and sanitizes
|
|
* image payloads before replaying messages into the app-server projector.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import type { SessionEntry } from "openclaw/plugin-sdk/agent-sessions";
|
|
import {
|
|
buildSessionContext,
|
|
migrateSessionEntries,
|
|
parseSessionEntries,
|
|
} from "openclaw/plugin-sdk/agent-sessions";
|
|
import { readCodexSessionTranscriptEventsBeforeAdmission } from "openclaw/plugin-sdk/codex-session-transcript-runtime";
|
|
import {
|
|
getSessionEntry,
|
|
parseSqliteSessionFileMarker,
|
|
resolveTranscriptSessionKeyBySessionId,
|
|
type SqliteSessionFileMarker,
|
|
} from "openclaw/plugin-sdk/session-store-runtime";
|
|
import {
|
|
readSessionTranscriptEvents,
|
|
type TranscriptTurnAdmission,
|
|
type SessionTranscriptTargetParams,
|
|
} from "openclaw/plugin-sdk/session-transcript-runtime";
|
|
import { sanitizeCodexHistoryImagePayloads } from "./image-payload-sanitizer.js";
|
|
|
|
function isMissingFileError(error: unknown): boolean {
|
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
}
|
|
|
|
export type CodexMirroredSessionHistoryTarget = {
|
|
agentId?: string;
|
|
sessionFile: string;
|
|
sessionId: string;
|
|
sessionKey?: string;
|
|
sessionTarget?: Partial<SessionTranscriptTargetParams>;
|
|
};
|
|
|
|
/** Returns sanitized session-context messages for a Codex mirrored session file. */
|
|
export async function readCodexMirroredSessionHistoryMessages(
|
|
target: CodexMirroredSessionHistoryTarget,
|
|
admission?: TranscriptTurnAdmission,
|
|
): Promise<AgentMessage[] | undefined> {
|
|
try {
|
|
const entries = await readCodexMirroredSessionEntries(target, admission);
|
|
if (entries.length === 0) {
|
|
return [];
|
|
}
|
|
const firstEntry = entries[0] as { type?: unknown; id?: unknown } | undefined;
|
|
if (firstEntry?.type !== "session") {
|
|
// A well-formed transcript that does not open with a `session` marker is
|
|
// simply not a Codex-mirrored session (e.g. a non-Codex model run reusing
|
|
// this hook) — an empty mirror, not a read failure, so callers must not
|
|
// warn. `undefined` stays reserved for genuine failures: read/parse errors
|
|
// (caught below) and malformed `session` headers (next check).
|
|
return [];
|
|
}
|
|
if (typeof firstEntry.id !== "string") {
|
|
// A `session` header without a string id is a corrupted Codex transcript,
|
|
// not a foreign one — keep it on the warn path.
|
|
return undefined;
|
|
}
|
|
if (firstEntry.id !== target.sessionId) {
|
|
return [];
|
|
}
|
|
migrateSessionEntries(entries);
|
|
const sessionEntries = entries.filter((entry): entry is SessionEntry => {
|
|
return (
|
|
entry !== null &&
|
|
typeof entry === "object" &&
|
|
!Array.isArray(entry) &&
|
|
(entry as { type?: unknown }).type !== "session"
|
|
);
|
|
});
|
|
return sanitizeCodexHistoryImagePayloads(
|
|
buildSessionContext(sessionEntries).messages,
|
|
"codex mirrored history",
|
|
);
|
|
} catch (error) {
|
|
// A new Codex session can be read before its transcript exists; other failures still warn.
|
|
if (isMissingFileError(error)) {
|
|
return [];
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
async function readCodexMirroredSessionEntries(
|
|
target: CodexMirroredSessionHistoryTarget,
|
|
admission?: TranscriptTurnAdmission,
|
|
): Promise<SessionEntry[]> {
|
|
if (target.sessionTarget) {
|
|
const { agentId, sessionId, sessionKey, storePath } = target.sessionTarget;
|
|
if (
|
|
!agentId ||
|
|
!sessionId ||
|
|
!sessionKey ||
|
|
!storePath ||
|
|
sessionId !== target.sessionId ||
|
|
(target.agentId !== undefined && agentId !== target.agentId) ||
|
|
(target.sessionKey !== undefined && sessionKey !== target.sessionKey)
|
|
) {
|
|
return [];
|
|
}
|
|
const transcriptTarget = {
|
|
agentId,
|
|
sessionId,
|
|
sessionKey,
|
|
storePath,
|
|
};
|
|
return (await (admission
|
|
? readCodexSessionTranscriptEventsBeforeAdmission(transcriptTarget, admission)
|
|
: readSessionTranscriptEvents(transcriptTarget))) as SessionEntry[];
|
|
}
|
|
const sqliteMarker = parseSqliteSessionFileMarker(target.sessionFile);
|
|
if (sqliteMarker) {
|
|
if (
|
|
sqliteMarker.sessionId !== target.sessionId ||
|
|
(target.agentId !== undefined && sqliteMarker.agentId !== target.agentId)
|
|
) {
|
|
return [];
|
|
}
|
|
const sessionKey = resolveSqliteMarkerSessionKey(target, sqliteMarker);
|
|
if (!sessionKey) {
|
|
return [];
|
|
}
|
|
const transcriptTarget = {
|
|
agentId: sqliteMarker.agentId,
|
|
sessionId: sqliteMarker.sessionId,
|
|
sessionKey,
|
|
storePath: sqliteMarker.storePath,
|
|
};
|
|
return (await (admission
|
|
? readCodexSessionTranscriptEventsBeforeAdmission(transcriptTarget, admission)
|
|
: readSessionTranscriptEvents(transcriptTarget))) as SessionEntry[];
|
|
}
|
|
if (admission) {
|
|
if (
|
|
admission.sessionId !== target.sessionId ||
|
|
(target.agentId !== undefined && admission.agentId !== target.agentId) ||
|
|
(target.sessionKey !== undefined && admission.sessionKey !== target.sessionKey)
|
|
) {
|
|
return [];
|
|
}
|
|
return (await readCodexSessionTranscriptEventsBeforeAdmission(
|
|
{
|
|
agentId: admission.agentId,
|
|
sessionId: admission.sessionId,
|
|
sessionKey: admission.sessionKey,
|
|
storePath: admission.storePath,
|
|
},
|
|
admission,
|
|
)) as SessionEntry[];
|
|
}
|
|
return parseSessionEntries(await fs.readFile(target.sessionFile, "utf-8")) as SessionEntry[];
|
|
}
|
|
|
|
function resolveSqliteMarkerSessionKey(
|
|
target: CodexMirroredSessionHistoryTarget,
|
|
marker: SqliteSessionFileMarker,
|
|
): string | undefined {
|
|
const explicitSessionKey = target.sessionKey?.trim();
|
|
if (explicitSessionKey) {
|
|
// The SDK exact-entry accessor uses a read-only database handle.
|
|
const explicitEntry = getSessionEntry({
|
|
agentId: marker.agentId,
|
|
sessionKey: explicitSessionKey,
|
|
storePath: marker.storePath,
|
|
});
|
|
if (explicitEntry) {
|
|
return explicitEntry.sessionId === marker.sessionId ? explicitSessionKey : undefined;
|
|
}
|
|
}
|
|
return resolveTranscriptSessionKeyBySessionId({
|
|
agentId: marker.agentId,
|
|
sessionId: marker.sessionId,
|
|
storePath: marker.storePath,
|
|
});
|
|
}
|