fix(copilot): keep tool transcript groups structurally complete (#114403)

* refactor(copilot): make SQLite own transcripts

* fix(copilot): harden transcript event projection

* test(copilot): tighten transcript journal types

* fix(copilot): retain unmatched usage metadata

* refactor(copilot): commit tool groups atomically

* fix(copilot): break journal bridge import cycle

* fix(copilot): preserve grouped transcript replay

* test(copilot): cover opaque replay state

* fix(copilot): invalidate lossy transcript replay

* fix(copilot): guard transcript rewrite topology

* fix(copilot): drain transcript finalization

* fix(copilot): preserve resumed session history

* fix(copilot): dedupe replayed transcript snapshots

* fix(copilot): isolate transcript write hooks

* fix(copilot): track staged transcript identities

* fix(copilot): isolate ephemeral transcript events

* fix(copilot): coalesce assistant snapshots

* fix(copilot): invalidate unprojected SDK context

* fix(copilot): guard resumed transcript replay

* fix(plugin-sdk): expose strict transcript append

* fix(copilot): persist journal provenance safely

* fix(copilot): separate live and durable deltas

* fix(copilot): require durable projection ownership

* fix(copilot): preserve strict append typing

* fix(copilot): gate deferred session deletion

* fix(copilot): preserve transcript replay identity

* fix(copilot): retain cited replay history

* fix(copilot): require complete replay provenance

* fix(copilot): reject ambiguous assistant snapshots

* fix(copilot): return latest assistant snapshot

* chore(copilot): align transcript lint gates

* test(lint): cover Copilot line budgets

* test(ci): dedupe Codex prewarm shard
This commit is contained in:
Peter Steinberger
2026-07-27 15:42:00 -04:00
committed by GitHub
parent bbc73c36bd
commit 3dc3370d9e
26 changed files with 3894 additions and 1287 deletions
@@ -2,10 +2,12 @@ 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,
@@ -128,6 +130,20 @@ export type SessionTranscriptTarget = SessionTranscriptIdentity & {
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;
@@ -345,6 +361,57 @@ export async function appendSessionTranscriptMessageByIdentity<TMessage>(
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.
*/