Fix runtime guidance transcript replay
This commit is contained in:
@@ -49,6 +49,7 @@ type ClawSession = {
|
||||
type ClawStoredMessage = {
|
||||
role?: string;
|
||||
content?: string;
|
||||
message_id?: string;
|
||||
name?: string;
|
||||
tool_call_id?: string;
|
||||
tool_calls?: ClawStoredToolCall[];
|
||||
@@ -801,7 +802,7 @@ export function toReplayRepository(
|
||||
: [];
|
||||
if (!content && !toolParts.length) continue;
|
||||
if (content.trimStart().startsWith("<system-reminder>")) continue;
|
||||
const id = `${fallbackSessionId}-${index}`;
|
||||
const id = replayMessageId(fallbackSessionId, message, index);
|
||||
const shouldShowAssistantText =
|
||||
message.role !== "assistant" ||
|
||||
!toolParts.length ||
|
||||
@@ -1185,6 +1186,18 @@ function isRunStatusMessage(message?: ClawStoredMessage) {
|
||||
return message?.metadata?.kind === "run_status";
|
||||
}
|
||||
|
||||
function replayMessageId(
|
||||
sessionId: string,
|
||||
message: ClawStoredMessage,
|
||||
index: number,
|
||||
) {
|
||||
const stableId =
|
||||
typeof message.message_id === "string" && message.message_id.trim()
|
||||
? message.message_id.trim()
|
||||
: String(index);
|
||||
return `${sessionId}-${stableId}`;
|
||||
}
|
||||
|
||||
function resolvePendingPrompt(
|
||||
messages: readonly ClawStoredMessage[],
|
||||
runStatus?: ClawRunStatus | null,
|
||||
@@ -1194,16 +1207,12 @@ function resolvePendingPrompt(
|
||||
? runStatus.pending_prompt.trim()
|
||||
: "";
|
||||
if (!pendingPrompt) return null;
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (!message || message.role === "system" || message.role === "tool")
|
||||
continue;
|
||||
for (const message of messages) {
|
||||
if (!message || message.role !== "user") continue;
|
||||
const content = cleanStoredContent(message.content ?? "").trim();
|
||||
if (!content || content.trimStart().startsWith("<system-reminder>"))
|
||||
continue;
|
||||
return message.role === "user" && content === pendingPrompt
|
||||
? null
|
||||
: pendingPrompt;
|
||||
if (content === pendingPrompt) return null;
|
||||
}
|
||||
return pendingPrompt;
|
||||
}
|
||||
|
||||
+94
-5
@@ -662,14 +662,43 @@ def _sync_agent_display_messages(
|
||||
if str(row['message_key'] or '')
|
||||
}
|
||||
next_seq = max(existing.keys(), default=0) + 1
|
||||
seq_offset = 0
|
||||
keep_seqs: set[int] = set()
|
||||
for seq, row in existing.items():
|
||||
try:
|
||||
existing_message = json.loads(row['message_json'])
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
existing_message = {}
|
||||
if isinstance(existing_message, dict) and _is_persistent_display_message(
|
||||
existing_message
|
||||
):
|
||||
keep_seqs.add(seq)
|
||||
for index, message in enumerate(display_messages, start=1):
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
message_json = json.dumps(message, ensure_ascii=False, sort_keys=True)
|
||||
message_key = _display_message_key(message)
|
||||
row = existing.get(index)
|
||||
target_seq = index + seq_offset
|
||||
row = existing.get(target_seq)
|
||||
while row is not None:
|
||||
try:
|
||||
old_target_message = json.loads(row['message_json'])
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
old_target_message = {}
|
||||
if not isinstance(old_target_message, dict):
|
||||
old_target_message = {}
|
||||
if not _is_persistent_display_message(old_target_message):
|
||||
break
|
||||
# Display-only messages, such as runtime guidance, are inserted
|
||||
# outside the model transcript. Later full-session syncs must
|
||||
# preserve them and shift model messages after them; otherwise
|
||||
# the UI sees an old pending prompt as a new user branch.
|
||||
seq_offset += 1
|
||||
target_seq = index + seq_offset
|
||||
row = existing.get(target_seq)
|
||||
keyed_row = existing_by_key.get(message_key)
|
||||
if keyed_row is not None and keyed_row['message_json'] == message_json:
|
||||
keep_seqs.add(int(keyed_row['seq']))
|
||||
continue
|
||||
if keyed_row is not None:
|
||||
try:
|
||||
@@ -695,6 +724,14 @@ def _sync_agent_display_messages(
|
||||
int(keyed_row['seq']),
|
||||
),
|
||||
)
|
||||
updated_row = {
|
||||
'seq': int(keyed_row['seq']),
|
||||
'message_key': message_key,
|
||||
'message_json': message_json,
|
||||
}
|
||||
existing[int(keyed_row['seq'])] = updated_row # type: ignore[assignment]
|
||||
existing_by_key[message_key] = updated_row # type: ignore[assignment]
|
||||
keep_seqs.add(int(keyed_row['seq']))
|
||||
continue
|
||||
if row is None:
|
||||
conn.execute(
|
||||
@@ -706,15 +743,25 @@ def _sync_agent_display_messages(
|
||||
""",
|
||||
(
|
||||
session_id,
|
||||
index,
|
||||
target_seq,
|
||||
message_key,
|
||||
str(message.get('role') or ''),
|
||||
now,
|
||||
message_json,
|
||||
),
|
||||
)
|
||||
inserted_row = {
|
||||
'seq': target_seq,
|
||||
'message_key': message_key,
|
||||
'message_json': message_json,
|
||||
}
|
||||
existing[target_seq] = inserted_row # type: ignore[assignment]
|
||||
existing_by_key[message_key] = inserted_row # type: ignore[assignment]
|
||||
next_seq = max(next_seq, target_seq + 1)
|
||||
keep_seqs.add(target_seq)
|
||||
continue
|
||||
if row['message_json'] == message_json:
|
||||
keep_seqs.add(target_seq)
|
||||
continue
|
||||
try:
|
||||
old_message = json.loads(row['message_json'])
|
||||
@@ -747,11 +794,14 @@ def _sync_agent_display_messages(
|
||||
message_json,
|
||||
),
|
||||
)
|
||||
existing_by_key[message_key] = {
|
||||
appended_row = {
|
||||
'seq': append_seq,
|
||||
'message_key': message_key,
|
||||
'message_json': message_json,
|
||||
} # type: ignore[assignment]
|
||||
}
|
||||
existing[append_seq] = appended_row # type: ignore[assignment]
|
||||
existing_by_key[message_key] = appended_row # type: ignore[assignment]
|
||||
keep_seqs.add(append_seq)
|
||||
continue
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -768,9 +818,32 @@ def _sync_agent_display_messages(
|
||||
now,
|
||||
message_json,
|
||||
session_id,
|
||||
index,
|
||||
target_seq,
|
||||
),
|
||||
)
|
||||
updated_row = {
|
||||
'seq': target_seq,
|
||||
'message_key': message_key,
|
||||
'message_json': message_json,
|
||||
}
|
||||
existing[target_seq] = updated_row # type: ignore[assignment]
|
||||
existing_by_key[message_key] = updated_row # type: ignore[assignment]
|
||||
keep_seqs.add(target_seq)
|
||||
stale_seqs = [
|
||||
seq
|
||||
for seq, row in existing.items()
|
||||
if seq not in keep_seqs
|
||||
and not _is_persistent_display_row(row)
|
||||
]
|
||||
if stale_seqs:
|
||||
conn.execute(
|
||||
f"""
|
||||
delete from agent_display_messages
|
||||
where session_id = ?
|
||||
and seq in ({','.join('?' for _ in stale_seqs)})
|
||||
""",
|
||||
(session_id, *stale_seqs),
|
||||
)
|
||||
|
||||
|
||||
def _display_message_key(message: JSONDict) -> str:
|
||||
@@ -795,6 +868,22 @@ def _is_replaceable_display_message(message: JSONDict) -> bool:
|
||||
return metadata.get('kind') in {'run_status'}
|
||||
|
||||
|
||||
def _is_persistent_display_message(message: JSONDict) -> bool:
|
||||
metadata = message.get('metadata')
|
||||
return (
|
||||
isinstance(metadata, dict)
|
||||
and metadata.get('kind') in {'runtime_guidance_display'}
|
||||
)
|
||||
|
||||
|
||||
def _is_persistent_display_row(row: sqlite3.Row | dict[str, Any]) -> bool:
|
||||
try:
|
||||
message = json.loads(row['message_json'])
|
||||
except (TypeError, json.JSONDecodeError, KeyError):
|
||||
return False
|
||||
return isinstance(message, dict) and _is_persistent_display_message(message)
|
||||
|
||||
|
||||
def _same_role_and_content(left: JSONDict, right: JSONDict) -> bool:
|
||||
return (
|
||||
str(left.get('role') or '') == str(right.get('role') or '')
|
||||
|
||||
Reference in New Issue
Block a user