mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 23:41:35 +00:00
576 lines
20 KiB
TypeScript
576 lines
20 KiB
TypeScript
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import { redactTranscriptMessage } from "../agents/transcript-redact.js";
|
|
import {
|
|
appendTranscriptMessage,
|
|
appendTranscriptMessages,
|
|
isSessionTranscriptProjectionUnavailableError,
|
|
loadSessionEntry,
|
|
loadTranscriptEvents,
|
|
publishTranscriptUpdate,
|
|
persistSessionTranscriptTurn,
|
|
readTranscriptRawDelta,
|
|
readSessionTranscriptVisibleMessageDelta as readVisibleMessageDelta,
|
|
readLatestTranscriptAssistantText,
|
|
resolveSessionTranscriptRuntimeReadTarget,
|
|
resolveSessionTranscriptRuntimeTarget,
|
|
withTranscriptWriteLock,
|
|
type TranscriptMessageAppendOptions,
|
|
type TranscriptMessageAppendResult,
|
|
type TranscriptUpdatePayload,
|
|
type SessionTranscriptRawDeltaLimits,
|
|
type SessionTranscriptRawDeltaResult,
|
|
type SessionTranscriptVisibleMessageDeltaLimits,
|
|
} from "../config/sessions/session-accessor.js";
|
|
import { resolveMirroredTranscriptText } from "../config/sessions/transcript-mirror.js";
|
|
import {
|
|
selectVisibleTranscriptEventEntries,
|
|
selectVisibleTranscriptEvents,
|
|
} from "../config/sessions/transcript-visible-events.js";
|
|
import type {
|
|
LatestAssistantTranscriptText,
|
|
SessionTranscriptAppendResult,
|
|
SessionTranscriptAssistantMessage,
|
|
SessionTranscriptDeliveryMirror,
|
|
SessionTranscriptUpdateMode,
|
|
} from "../config/sessions/transcript.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizeAgentId } from "../routing/session-key.js";
|
|
import { extractAssistantVisibleText } from "../shared/chat-message-content.js";
|
|
import type { AgentMessage } from "./agent-core.js";
|
|
import { withProjectedSessionTranscriptWriteLock } from "./session-transcript-lock-runtime.js";
|
|
import {
|
|
formatSessionTranscriptMemoryHitKey,
|
|
parseSessionTranscriptMemoryHitKey,
|
|
resolveSessionTranscriptMemoryHitKeyToSessionKeys,
|
|
type ResolveSessionTranscriptMemoryHitKeyParams,
|
|
type SessionTranscriptIdentity,
|
|
type SessionTranscriptMemoryHitIdentity,
|
|
type SessionTranscriptMemoryHitKey,
|
|
type SessionTranscriptMemoryHitKeyParams,
|
|
type SessionTranscriptReadParams,
|
|
} from "./session-transcript-memory-hit.js";
|
|
|
|
export {
|
|
formatSessionTranscriptMemoryHitKey,
|
|
parseSessionTranscriptMemoryHitKey,
|
|
resolveSessionTranscriptMemoryHitKeyToSessionKeys,
|
|
};
|
|
export type {
|
|
ResolveSessionTranscriptMemoryHitKeyParams,
|
|
SessionTranscriptIdentity,
|
|
SessionTranscriptMemoryHitIdentity,
|
|
SessionTranscriptMemoryHitKey,
|
|
SessionTranscriptMemoryHitKeyParams,
|
|
SessionTranscriptReadParams,
|
|
};
|
|
|
|
export type SessionTranscriptEvent = unknown;
|
|
|
|
export type SessionTranscriptTargetParams = SessionTranscriptReadParams;
|
|
|
|
/** Scoped target and bounds for one raw generation-aware transcript page. */
|
|
export type SessionTranscriptRawDeltaParams = SessionTranscriptTargetParams &
|
|
SessionTranscriptRawDeltaLimits;
|
|
export type { SessionTranscriptRawDeltaResult };
|
|
|
|
/** Scoped target and bounds for one active-path visible-message page. */
|
|
export type SessionTranscriptVisibleMessageDeltaParams = SessionTranscriptTargetParams &
|
|
SessionTranscriptVisibleMessageDeltaLimits;
|
|
|
|
/** Generation-aware outcome for one bounded visible-message read. */
|
|
export type SessionTranscriptVisibleMessageDeltaResult =
|
|
| {
|
|
kind: "page";
|
|
/** Opaque cursor positioned after the last returned visible message. */
|
|
cursor: string;
|
|
/** Ordered active-path message entries selected for this page. */
|
|
entries: SessionTranscriptMessageEntry[];
|
|
/** True when another visible message remains after this page. */
|
|
hasMore: boolean;
|
|
/** First unread event size when it cannot fit under maxBytes. */
|
|
requiredBytes?: number;
|
|
/** Stored JSONL bytes represented by entries. */
|
|
serializedBytes: number;
|
|
}
|
|
| {
|
|
kind: "reset";
|
|
/** Fresh opaque bootstrap cursor for the current visible generation. */
|
|
cursor: string;
|
|
/** Stable discontinuity that invalidated the supplied cursor. */
|
|
reason:
|
|
| "anchor_missing"
|
|
| "anchor_moved"
|
|
| "generation_mismatch"
|
|
| "invalid_cursor"
|
|
| "scope_mismatch";
|
|
}
|
|
| { kind: "unavailable"; reason: "projection_rebuilding" }
|
|
| { kind: "missing" };
|
|
|
|
export type SessionTranscriptMessageEntry = {
|
|
/** Stable transcript event id for this message entry. */
|
|
entryId: string;
|
|
/** Parent id after active-branch normalization; null when this is a visible root. */
|
|
parentId: string | null;
|
|
/** Ordered read metadata for this full transcript read, not a resumable cursor. */
|
|
seq: number;
|
|
/** Redacted agent message payload as persisted by the runtime. */
|
|
message: AgentMessage;
|
|
/** Convenience mirror of message.role. */
|
|
role: AgentMessage["role"];
|
|
/** Entry timestamp recorded by the transcript store, when present. */
|
|
createdAt?: string;
|
|
/** Message idempotency key, when the persisted message has one. */
|
|
idempotencyKey?: string;
|
|
};
|
|
|
|
export type SessionTranscriptTarget = SessionTranscriptIdentity & {
|
|
targetKind: "runtime-session";
|
|
};
|
|
|
|
export type SessionTranscriptAppendMessageParams<TMessage> = SessionTranscriptTargetParams &
|
|
TranscriptMessageAppendOptions<TMessage>;
|
|
|
|
export type SessionTranscriptAppendMessagesParams<TMessage> = SessionTranscriptTargetParams & {
|
|
config?: TranscriptMessageAppendOptions<TMessage>["config"];
|
|
cwd?: string;
|
|
messages: readonly Omit<
|
|
TranscriptMessageAppendOptions<TMessage>,
|
|
"config" | "cwd" | "parentId" | "prepareMessageAfterIdempotencyCheck" | "useRawWhenLinear"
|
|
>[];
|
|
};
|
|
|
|
export type SessionTranscriptStrictMessageAppendResult<TMessage> =
|
|
| { kind: "result"; result: TranscriptMessageAppendResult<TMessage> }
|
|
| { kind: "suppressed" }
|
|
| { kind: "rejected"; reason: "session-rebound" };
|
|
|
|
export type SessionTranscriptAssistantMirrorAppendParams = SessionTranscriptReadParams & {
|
|
config?: OpenClawConfig;
|
|
deliveryMirror?: SessionTranscriptDeliveryMirror;
|
|
idempotencyKey?: string;
|
|
mediaUrls?: string[];
|
|
text?: string;
|
|
updateMode?: SessionTranscriptUpdateMode;
|
|
};
|
|
|
|
export type SessionTranscriptWriteLockParams = SessionTranscriptTargetParams & {
|
|
config?: TranscriptMessageAppendOptions<unknown>["config"];
|
|
};
|
|
|
|
export type SessionTranscriptWriteLockContext = {
|
|
appendMessage: <TMessage>(
|
|
options: Omit<TranscriptMessageAppendOptions<TMessage>, "config">,
|
|
) => Promise<TranscriptMessageAppendResult<TMessage> | undefined>;
|
|
publishUpdate: (update?: TranscriptUpdatePayload) => Promise<void>;
|
|
readEvents: () => Promise<SessionTranscriptEvent[]>;
|
|
target: SessionTranscriptTarget;
|
|
};
|
|
|
|
type SessionTranscriptMirrorAppendResult =
|
|
| { ok: true; messageId: string }
|
|
| Extract<SessionTranscriptAppendResult, { ok: false }>;
|
|
|
|
/**
|
|
* Resolves the public identity for a transcript without returning its file path.
|
|
*/
|
|
export async function resolveSessionTranscriptIdentity(
|
|
params: SessionTranscriptReadParams,
|
|
): Promise<SessionTranscriptIdentity> {
|
|
const target = await resolveSessionTranscriptRuntimeReadTarget(params);
|
|
const agentId = normalizeAgentId(target.agentId);
|
|
return {
|
|
agentId,
|
|
memoryKey: formatSessionTranscriptMemoryHitKey({ agentId, sessionId: target.sessionId }),
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolves the public target for transcript operations without exposing the
|
|
* current storage path as identity.
|
|
*/
|
|
export async function resolveSessionTranscriptTarget(
|
|
params: SessionTranscriptTargetParams,
|
|
): Promise<SessionTranscriptTarget> {
|
|
const target = await resolveSessionTranscriptRuntimeReadTarget(params);
|
|
return projectPublicTarget({
|
|
...target,
|
|
targetKind: "runtime-session",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reads transcript events by public session identity instead of file path.
|
|
*/
|
|
export async function readSessionTranscriptEvents(
|
|
params: SessionTranscriptTargetParams,
|
|
): Promise<SessionTranscriptEvent[]> {
|
|
return await loadTranscriptEvents(params);
|
|
}
|
|
|
|
/** Reads one bounded raw page; the opaque cursor survives append and resets after replacement. */
|
|
export async function readSessionTranscriptRawDelta(
|
|
params: SessionTranscriptRawDeltaParams,
|
|
): Promise<SessionTranscriptRawDeltaResult> {
|
|
const { cursor, maxBytes, maxEvents, ...target } = params;
|
|
return readTranscriptRawDelta(target, {
|
|
...(cursor !== undefined ? { cursor } : {}),
|
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
...(maxEvents !== undefined ? { maxEvents } : {}),
|
|
});
|
|
}
|
|
|
|
/** Reads one bounded active-path page that resumes appends and resets after discontinuities. */
|
|
export async function readSessionTranscriptVisibleMessageDelta(
|
|
params: SessionTranscriptVisibleMessageDeltaParams,
|
|
): Promise<SessionTranscriptVisibleMessageDeltaResult> {
|
|
const { cursor, maxBytes, maxMessages, ...target } = params;
|
|
let result: ReturnType<typeof readVisibleMessageDelta>;
|
|
try {
|
|
result = readVisibleMessageDelta(target, {
|
|
...(cursor !== undefined ? { cursor } : {}),
|
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
...(maxMessages !== undefined ? { maxMessages } : {}),
|
|
});
|
|
} catch (error) {
|
|
if (isSessionTranscriptProjectionUnavailableError(error)) {
|
|
return { kind: "unavailable", reason: "projection_rebuilding" };
|
|
}
|
|
throw error;
|
|
}
|
|
if (result.kind !== "page") {
|
|
return result;
|
|
}
|
|
const { events, ...page } = result;
|
|
return {
|
|
...page,
|
|
entries: events.flatMap((entry) =>
|
|
projectVisibleMessageEntry({
|
|
event: entry.event,
|
|
parentId: entry.parentId,
|
|
seq: entry.seq,
|
|
}),
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads visible transcript message entries by scoped identity.
|
|
*
|
|
* This is a branch-safe message projection over the current full transcript
|
|
* read. `seq` is ordered read metadata, not a resumable cursor.
|
|
*/
|
|
export async function readVisibleSessionTranscriptMessageEntries(
|
|
params: SessionTranscriptTargetParams,
|
|
): Promise<SessionTranscriptMessageEntry[]> {
|
|
return selectVisibleTranscriptEventEntries(await loadTranscriptEvents(params)).flatMap(
|
|
projectVisibleMessageEntry,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads the latest visible assistant text by scoped identity.
|
|
*/
|
|
export async function readLatestAssistantTextByIdentity(
|
|
params: SessionTranscriptTargetParams,
|
|
): Promise<LatestAssistantTranscriptText | undefined> {
|
|
return readLatestTranscriptAssistantText(params);
|
|
}
|
|
|
|
/**
|
|
* Appends a delivery-mirror assistant message through the SQLite transcript accessor.
|
|
*/
|
|
export async function appendAssistantMirrorMessageByIdentity(
|
|
params: SessionTranscriptAssistantMirrorAppendParams,
|
|
): Promise<SessionTranscriptMirrorAppendResult> {
|
|
const text = resolveMirroredTranscriptText({
|
|
...(params.mediaUrls !== undefined ? { mediaUrls: params.mediaUrls } : {}),
|
|
...(params.text !== undefined ? { text: params.text } : {}),
|
|
});
|
|
if (!text) {
|
|
return { ok: false, reason: "empty message" };
|
|
}
|
|
const message = createAssistantMirrorMessage({
|
|
...(params.deliveryMirror !== undefined ? { deliveryMirror: params.deliveryMirror } : {}),
|
|
...(params.idempotencyKey !== undefined ? { idempotencyKey: params.idempotencyKey } : {}),
|
|
text,
|
|
});
|
|
return await withTranscriptWriteLock(params, async (locked) => {
|
|
const currentEntry = loadSessionEntry(params);
|
|
if (!currentEntry?.sessionId) {
|
|
return { ok: false, reason: "missing active session", code: "blocked" };
|
|
}
|
|
if (params.sessionId && currentEntry.sessionId !== params.sessionId) {
|
|
return { ok: false, reason: "session changed", code: "session-rebound" };
|
|
}
|
|
const scope = {
|
|
...params,
|
|
sessionId: currentEntry.sessionId,
|
|
};
|
|
const target = await resolveSessionTranscriptRuntimeReadTarget(scope);
|
|
const latestEquivalentAssistantId =
|
|
!params.idempotencyKey && isDeliveryMirrorAssistantMessage(message)
|
|
? findLatestEquivalentAssistantMessageId(
|
|
selectVisibleTranscriptEvents(await locked.readEvents()),
|
|
message,
|
|
params.config,
|
|
)
|
|
: undefined;
|
|
if (latestEquivalentAssistantId) {
|
|
return {
|
|
ok: true,
|
|
messageId: latestEquivalentAssistantId,
|
|
};
|
|
}
|
|
const appendResult = await locked.appendMessage({
|
|
...(params.config !== undefined ? { config: params.config } : {}),
|
|
...(params.idempotencyKey ? { idempotencyLookup: "scan" as const } : {}),
|
|
message,
|
|
});
|
|
if (!appendResult) {
|
|
return { ok: false, reason: "message skipped", code: "blocked" };
|
|
}
|
|
if (params.updateMode !== "none" && appendResult.appended) {
|
|
await publishTranscriptUpdate(scope, {
|
|
agentId: target.agentId,
|
|
messageId: appendResult.messageId,
|
|
sessionKey: target.sessionKey,
|
|
target: {
|
|
agentId: target.agentId,
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
},
|
|
});
|
|
}
|
|
return {
|
|
ok: true,
|
|
messageId: appendResult.messageId,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Appends an already-canonical transcript message by scoped transcript target.
|
|
* Media-bearing user turns use ordered `message.__openclaw.media` facts; this
|
|
* low-level API does not infer deprecated top-level Media* projections.
|
|
*/
|
|
export async function appendSessionTranscriptMessageByIdentity<TMessage>(
|
|
params: SessionTranscriptAppendMessageParams<TMessage>,
|
|
): Promise<TranscriptMessageAppendResult<TMessage> | undefined> {
|
|
return await appendTranscriptMessage(params, params);
|
|
}
|
|
|
|
/** Appends one message while preserving distinct suppression and session-rebind outcomes. */
|
|
export async function appendSessionTranscriptMessageByIdentityStrict<TMessage>(
|
|
params: SessionTranscriptAppendMessageParams<TMessage>,
|
|
): Promise<SessionTranscriptStrictMessageAppendResult<TMessage>> {
|
|
const expectedSessionId = params.sessionId?.trim();
|
|
if (!expectedSessionId) {
|
|
throw new Error("Cannot strictly append a transcript message without an exact session id");
|
|
}
|
|
const turn = await persistSessionTranscriptTurn(params, {
|
|
...(params.config ? { config: params.config } : {}),
|
|
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
expectedSessionId,
|
|
messages: [
|
|
{
|
|
...(params.eventId !== undefined ? { eventId: params.eventId } : {}),
|
|
...(params.idempotencyLookup !== undefined
|
|
? { idempotencyLookup: params.idempotencyLookup }
|
|
: {}),
|
|
message: params.message,
|
|
...(params.now !== undefined ? { now: params.now } : {}),
|
|
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
|
|
...(params.prepareMessageAfterIdempotencyCheck
|
|
? {
|
|
prepareMessageAfterIdempotencyCheck: (message: unknown) =>
|
|
params.prepareMessageAfterIdempotencyCheck?.(message as TMessage),
|
|
}
|
|
: {}),
|
|
...(params.useRawWhenLinear !== undefined
|
|
? { useRawWhenLinear: params.useRawWhenLinear }
|
|
: {}),
|
|
},
|
|
],
|
|
updateMode: "none",
|
|
});
|
|
if (turn.rejectedReason) {
|
|
return { kind: "rejected", reason: turn.rejectedReason };
|
|
}
|
|
const result = turn.messages[0] as TranscriptMessageAppendResult<TMessage> | undefined;
|
|
return result ? { kind: "result", result } : { kind: "suppressed" };
|
|
}
|
|
|
|
/**
|
|
* Atomically appends one ordered, already-hooked message group. Preparation and
|
|
* redaction finish before SQLite begins; this is the canonical future harness seam.
|
|
*/
|
|
export async function appendSessionTranscriptMessagesByIdentity<TMessage>(
|
|
params: SessionTranscriptAppendMessagesParams<TMessage>,
|
|
): Promise<TranscriptMessageAppendResult<TMessage>[]> {
|
|
return await appendTranscriptMessages(params, params);
|
|
}
|
|
|
|
/**
|
|
* Publishes a transcript update by scoped transcript target.
|
|
*/
|
|
export async function publishSessionTranscriptUpdateByIdentity(
|
|
params: SessionTranscriptTargetParams & { update?: TranscriptUpdatePayload },
|
|
): Promise<void> {
|
|
const target = await resolveSessionTranscriptRuntimeTarget(params);
|
|
await publishTranscriptUpdate(
|
|
{
|
|
...params,
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
},
|
|
{
|
|
...params.update,
|
|
agentId: target.agentId,
|
|
sessionKey: target.sessionKey,
|
|
target: {
|
|
agentId: target.agentId,
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Runs transcript work under the write lock for the resolved scoped target.
|
|
*/
|
|
export async function withSessionTranscriptWriteLock<T>(
|
|
params: SessionTranscriptWriteLockParams,
|
|
run: (context: SessionTranscriptWriteLockContext) => Promise<T> | T,
|
|
): Promise<T> {
|
|
return await withProjectedSessionTranscriptWriteLock(params, run, (context) => context);
|
|
}
|
|
|
|
function createAssistantMirrorMessage(params: {
|
|
deliveryMirror?: SessionTranscriptDeliveryMirror;
|
|
idempotencyKey?: string;
|
|
text: string;
|
|
}): SessionTranscriptAssistantMessage {
|
|
return {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: params.text }],
|
|
api: "openai-responses",
|
|
provider: "openclaw",
|
|
model: "delivery-mirror",
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: Date.now(),
|
|
...(params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}),
|
|
...(params.deliveryMirror ? { openclawDeliveryMirror: params.deliveryMirror } : {}),
|
|
};
|
|
}
|
|
|
|
function findLatestEquivalentAssistantMessageId(
|
|
events: readonly SessionTranscriptEvent[],
|
|
message: SessionTranscriptAssistantMessage,
|
|
config: OpenClawConfig | undefined,
|
|
): string | undefined {
|
|
const expectedText = extractAssistantMirrorComparableText(message, config);
|
|
if (!expectedText) {
|
|
return undefined;
|
|
}
|
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
const event = events[index];
|
|
if (!event || typeof event !== "object") {
|
|
continue;
|
|
}
|
|
const record = event as { id?: unknown; message?: unknown };
|
|
const candidate = record.message as SessionTranscriptAssistantMessage | undefined;
|
|
if (!candidate) {
|
|
continue;
|
|
}
|
|
if (candidate.role !== "assistant") {
|
|
return undefined;
|
|
}
|
|
return extractAssistantMirrorComparableText(candidate, config) === expectedText &&
|
|
typeof record.id === "string" &&
|
|
record.id
|
|
? record.id
|
|
: undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractAssistantMirrorComparableText(
|
|
message: SessionTranscriptAssistantMessage,
|
|
config: OpenClawConfig | undefined,
|
|
): string | undefined {
|
|
const redacted = redactTranscriptMessage(
|
|
message as Parameters<typeof redactTranscriptMessage>[0],
|
|
config,
|
|
) as SessionTranscriptAssistantMessage;
|
|
return extractAssistantVisibleText(redacted)?.trim() || undefined;
|
|
}
|
|
|
|
function isDeliveryMirrorAssistantMessage(message: SessionTranscriptAssistantMessage): boolean {
|
|
return message.provider === "openclaw" && message.model === "delivery-mirror";
|
|
}
|
|
|
|
function readNonEmptyString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
}
|
|
|
|
function isAgentMessageRecord(value: unknown): value is AgentMessage & Record<string, unknown> {
|
|
return isRecord(value) && readNonEmptyString(value.role) !== undefined;
|
|
}
|
|
|
|
function projectVisibleMessageEntry(entry: {
|
|
event: SessionTranscriptEvent;
|
|
parentId: string | null;
|
|
seq: number;
|
|
}): SessionTranscriptMessageEntry[] {
|
|
const event = entry.event;
|
|
if (!isRecord(event) || event.type !== "message") {
|
|
return [];
|
|
}
|
|
const entryId = readNonEmptyString(event.id);
|
|
const message = event.message;
|
|
if (!entryId || !isAgentMessageRecord(message)) {
|
|
return [];
|
|
}
|
|
const createdAt = readNonEmptyString(event.timestamp);
|
|
const idempotencyKey = readNonEmptyString(message.idempotencyKey);
|
|
return [
|
|
{
|
|
entryId,
|
|
parentId: entry.parentId,
|
|
seq: entry.seq,
|
|
message,
|
|
role: message.role,
|
|
...(createdAt ? { createdAt } : {}),
|
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
},
|
|
];
|
|
}
|
|
|
|
function projectPublicTarget(target: {
|
|
agentId: string;
|
|
sessionId: string;
|
|
sessionKey: string;
|
|
targetKind: SessionTranscriptTarget["targetKind"];
|
|
}): SessionTranscriptTarget {
|
|
const agentId = normalizeAgentId(target.agentId);
|
|
return {
|
|
agentId,
|
|
memoryKey: formatSessionTranscriptMemoryHitKey({ agentId, sessionId: target.sessionId }),
|
|
sessionId: target.sessionId,
|
|
sessionKey: target.sessionKey,
|
|
targetKind: target.targetKind,
|
|
};
|
|
}
|