Fix runtime guidance transcript replay
This commit is contained in:
@@ -49,6 +49,7 @@ type ClawSession = {
|
|||||||
type ClawStoredMessage = {
|
type ClawStoredMessage = {
|
||||||
role?: string;
|
role?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
message_id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
tool_call_id?: string;
|
tool_call_id?: string;
|
||||||
tool_calls?: ClawStoredToolCall[];
|
tool_calls?: ClawStoredToolCall[];
|
||||||
@@ -801,7 +802,7 @@ export function toReplayRepository(
|
|||||||
: [];
|
: [];
|
||||||
if (!content && !toolParts.length) continue;
|
if (!content && !toolParts.length) continue;
|
||||||
if (content.trimStart().startsWith("<system-reminder>")) continue;
|
if (content.trimStart().startsWith("<system-reminder>")) continue;
|
||||||
const id = `${fallbackSessionId}-${index}`;
|
const id = replayMessageId(fallbackSessionId, message, index);
|
||||||
const shouldShowAssistantText =
|
const shouldShowAssistantText =
|
||||||
message.role !== "assistant" ||
|
message.role !== "assistant" ||
|
||||||
!toolParts.length ||
|
!toolParts.length ||
|
||||||
@@ -1185,6 +1186,18 @@ function isRunStatusMessage(message?: ClawStoredMessage) {
|
|||||||
return message?.metadata?.kind === "run_status";
|
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(
|
function resolvePendingPrompt(
|
||||||
messages: readonly ClawStoredMessage[],
|
messages: readonly ClawStoredMessage[],
|
||||||
runStatus?: ClawRunStatus | null,
|
runStatus?: ClawRunStatus | null,
|
||||||
@@ -1194,16 +1207,12 @@ function resolvePendingPrompt(
|
|||||||
? runStatus.pending_prompt.trim()
|
? runStatus.pending_prompt.trim()
|
||||||
: "";
|
: "";
|
||||||
if (!pendingPrompt) return null;
|
if (!pendingPrompt) return null;
|
||||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
for (const message of messages) {
|
||||||
const message = messages[index];
|
if (!message || message.role !== "user") continue;
|
||||||
if (!message || message.role === "system" || message.role === "tool")
|
|
||||||
continue;
|
|
||||||
const content = cleanStoredContent(message.content ?? "").trim();
|
const content = cleanStoredContent(message.content ?? "").trim();
|
||||||
if (!content || content.trimStart().startsWith("<system-reminder>"))
|
if (!content || content.trimStart().startsWith("<system-reminder>"))
|
||||||
continue;
|
continue;
|
||||||
return message.role === "user" && content === pendingPrompt
|
if (content === pendingPrompt) return null;
|
||||||
? null
|
|
||||||
: pendingPrompt;
|
|
||||||
}
|
}
|
||||||
return pendingPrompt;
|
return pendingPrompt;
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-5
@@ -662,14 +662,43 @@ def _sync_agent_display_messages(
|
|||||||
if str(row['message_key'] or '')
|
if str(row['message_key'] or '')
|
||||||
}
|
}
|
||||||
next_seq = max(existing.keys(), default=0) + 1
|
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):
|
for index, message in enumerate(display_messages, start=1):
|
||||||
if not isinstance(message, dict):
|
if not isinstance(message, dict):
|
||||||
continue
|
continue
|
||||||
message_json = json.dumps(message, ensure_ascii=False, sort_keys=True)
|
message_json = json.dumps(message, ensure_ascii=False, sort_keys=True)
|
||||||
message_key = _display_message_key(message)
|
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)
|
keyed_row = existing_by_key.get(message_key)
|
||||||
if keyed_row is not None and keyed_row['message_json'] == message_json:
|
if keyed_row is not None and keyed_row['message_json'] == message_json:
|
||||||
|
keep_seqs.add(int(keyed_row['seq']))
|
||||||
continue
|
continue
|
||||||
if keyed_row is not None:
|
if keyed_row is not None:
|
||||||
try:
|
try:
|
||||||
@@ -695,6 +724,14 @@ def _sync_agent_display_messages(
|
|||||||
int(keyed_row['seq']),
|
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
|
continue
|
||||||
if row is None:
|
if row is None:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -706,15 +743,25 @@ def _sync_agent_display_messages(
|
|||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
session_id,
|
session_id,
|
||||||
index,
|
target_seq,
|
||||||
message_key,
|
message_key,
|
||||||
str(message.get('role') or ''),
|
str(message.get('role') or ''),
|
||||||
now,
|
now,
|
||||||
message_json,
|
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
|
continue
|
||||||
if row['message_json'] == message_json:
|
if row['message_json'] == message_json:
|
||||||
|
keep_seqs.add(target_seq)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
old_message = json.loads(row['message_json'])
|
old_message = json.loads(row['message_json'])
|
||||||
@@ -747,11 +794,14 @@ def _sync_agent_display_messages(
|
|||||||
message_json,
|
message_json,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
existing_by_key[message_key] = {
|
appended_row = {
|
||||||
'seq': append_seq,
|
'seq': append_seq,
|
||||||
'message_key': message_key,
|
'message_key': message_key,
|
||||||
'message_json': message_json,
|
'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
|
continue
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -768,9 +818,32 @@ def _sync_agent_display_messages(
|
|||||||
now,
|
now,
|
||||||
message_json,
|
message_json,
|
||||||
session_id,
|
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:
|
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'}
|
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:
|
def _same_role_and_content(left: JSONDict, right: JSONDict) -> bool:
|
||||||
return (
|
return (
|
||||||
str(left.get('role') or '') == str(right.get('role') or '')
|
str(left.get('role') or '') == str(right.get('role') or '')
|
||||||
|
|||||||
Reference in New Issue
Block a user