Files
openclaw/src/sessions/user-turn-transcript-runtime-context.ts
T
pick-cat f296dc4e68 fix(sessions): persist sender metadata in user turn transcript JSONL (#90552)
* fix(sessions): persist sender metadata in user turn transcript JSONL

Thread senderId/senderName/senderUsername/senderE164 from the channel
inbound context into the persisted user-turn transcript message so that
group chat session JSONL records include __openclaw sender identity.

Ref #90531

* fix(sessions): exclude senderE164 from persisted transcript for privacy

Remove phone-number field from the persisted __openclaw sender envelope,
keeping only senderId, senderName, and senderUsername. Privacy-sensitive
E.164 metadata can be added back by maintainers if needed.

Ref #90531

* fix(infra): spread base fields in applyExecPolicyLayer return values

The `as TBase & ExecPolicyLayer` casts failed because the returned
objects did not spread `...base`, losing generic TBase fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(infra): use ...base spread instead of ...baseWithoutMode

The baseWithoutMode destructuring produces Omit<TBase, "mode"> which is
not assignable to TBase & ExecPolicyLayer when the as-cast is removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(infra): preserve baseWithoutMode in second applyExecPolicyLayer branch

The second branch (security/ask override without a mode change) must
exclude base.mode from the spread so that mode is not leaked into results
when only security or ask fields are being overridden.

Without this fix, the spread ...base carries mode through to the returned
object, breaking callers that expect applyExecPolicyLayer to clear stale
mode when applying explicit security/ask policy fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): scope persisted sender attribution

* fix(sessions): preserve sender metadata through hooks

* refactor(sessions): keep sender metadata path lean

* fix(sessions): preserve sender metadata in runtime writes

* fix(sessions): preserve queued sender attribution

* test(sessions): use complete message fixtures

* refactor(sessions): rely on narrowed user message type

* test(sessions): use shared temp directory helper

* test(sessions): align sender metadata assertions

* fix(sessions): honor sender metadata redaction hooks

* test(agents): use automatic temp cleanup

* test(sessions): cover queued turn provenance

* test(auto-reply): expect room sender metadata

* refactor(sessions): isolate queued transcript context

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-06 05:32:19 +01:00

70 lines
2.3 KiB
TypeScript

// Transient user-turn transcript context carried through runtime queues.
import type { AgentMessage } from "../../packages/agent-core/src/types.js";
import type {
PersistedUserTurnMessage,
UserTurnTranscriptRecorder,
} from "./user-turn-transcript.types.js";
const RUNTIME_USER_TURN_TRANSCRIPT_CONTEXT = Symbol.for(
"openclaw.runtimeUserTurnTranscriptContext",
);
const RUNTIME_USER_TURN_TRANSCRIPT_RECORDER = Symbol.for(
"openclaw.runtimeUserTurnTranscriptRecorder",
);
export type RuntimeUserTurnTranscriptContext = {
message: PersistedUserTurnMessage;
recorder: UserTurnTranscriptRecorder;
};
/** Carries transcript-only fields with a queued runtime message without exposing them to the model. */
export function attachRuntimeUserTurnTranscriptContext(
runtimeMessage: PersistedUserTurnMessage,
context: RuntimeUserTurnTranscriptContext,
): PersistedUserTurnMessage {
Object.defineProperty(runtimeMessage, RUNTIME_USER_TURN_TRANSCRIPT_CONTEXT, {
configurable: true,
value: context,
});
return runtimeMessage;
}
/** Consumes the transient queued-turn context before the message is serialized. */
export function takeRuntimeUserTurnTranscriptContext(
runtimeMessage: AgentMessage,
): RuntimeUserTurnTranscriptContext | undefined {
const record = runtimeMessage as unknown as Record<PropertyKey, unknown>;
const context = record[RUNTIME_USER_TURN_TRANSCRIPT_CONTEXT] as
| RuntimeUserTurnTranscriptContext
| undefined;
if (context) {
delete record[RUNTIME_USER_TURN_TRANSCRIPT_CONTEXT];
}
return context;
}
/** Keeps the queued recorder attached to the exact final message until persistence succeeds. */
export function attachRuntimeUserTurnTranscriptRecorder(
runtimeMessage: AgentMessage,
recorder: UserTurnTranscriptRecorder,
): AgentMessage {
Object.defineProperty(runtimeMessage, RUNTIME_USER_TURN_TRANSCRIPT_RECORDER, {
configurable: true,
value: recorder,
});
return runtimeMessage;
}
export function takeRuntimeUserTurnTranscriptRecorder(
runtimeMessage: AgentMessage,
): UserTurnTranscriptRecorder | undefined {
const record = runtimeMessage as unknown as Record<PropertyKey, unknown>;
const recorder = record[RUNTIME_USER_TURN_TRANSCRIPT_RECORDER] as
| UserTurnTranscriptRecorder
| undefined;
if (recorder) {
delete record[RUNTIME_USER_TURN_TRANSCRIPT_RECORDER];
}
return recorder;
}