diff --git a/backend/api/server.py b/backend/api/server.py index 76af425..3c49609 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -354,6 +354,7 @@ class RunRecord: updated_at: float cancel_event: threading.Event process_registry: RunProcessRegistry + pending_prompt: str = '' current_stage: str = '' error: str = '' events: list[dict[str, Any]] = field(default_factory=list) @@ -365,7 +366,7 @@ class RunManager: self._runs: dict[str, RunRecord] = {} self._latest_by_session: dict[tuple[str, str], str] = {} - def start(self, account_key: str, session_id: str) -> RunRecord: + def start(self, account_key: str, session_id: str, pending_prompt: str = '') -> RunRecord: now = time.time() record = RunRecord( run_id=uuid4().hex, @@ -376,6 +377,7 @@ class RunManager: updated_at=now, cancel_event=threading.Event(), process_registry=RunProcessRegistry(), + pending_prompt=pending_prompt, ) with self._lock: self._runs[record.run_id] = record @@ -442,6 +444,7 @@ class RunManager: 'current_stage': record.current_stage, 'started_at': record.started_at, 'updated_at': record.updated_at, + 'pending_prompt': record.pending_prompt, 'error': record.error, 'events': [dict(event) for event in record.events], } @@ -1303,7 +1306,8 @@ def create_app(state: AgentState) -> FastAPI: request.resume_session_id or request.session_id ) or uuid4().hex account_key = state._account_key(request.account_id) - run_record = state.run_manager.start(account_key, requested_session_id) + prompt = request.prompt.strip() + run_record = state.run_manager.start(account_key, requested_session_id, prompt) run_lock = state.run_lock_for(request.account_id, requested_session_id) agent = state.agent_for(request.account_id, requested_session_id) config = state.config_for(request.account_id) @@ -1326,7 +1330,7 @@ def create_app(state: AgentState) -> FastAPI: directory=session_directory, agent=agent, session_id=requested_session_id, - prompt=request.prompt.strip(), + prompt=prompt, ) if run_lock.locked(): queued_event = { @@ -1384,14 +1388,14 @@ def create_app(state: AgentState) -> FastAPI: ) stored = _sanitize_stored_session_for_resume(stored) result = agent.resume( - request.prompt.strip(), + prompt, stored, runtime_context=request.runtime_context, event_sink=emit_agent_event, ) else: result = agent.run( - request.prompt.strip(), + prompt, session_id=requested_session_id, runtime_context=request.runtime_context, event_sink=emit_agent_event, diff --git a/frontend/app/components/assistant-ui/thread-list.tsx b/frontend/app/components/assistant-ui/thread-list.tsx index c04871f..dd7c916 100644 --- a/frontend/app/components/assistant-ui/thread-list.tsx +++ b/frontend/app/components/assistant-ui/thread-list.tsx @@ -69,6 +69,7 @@ export type ClawRunStatus = { | "interrupted"; current_stage?: string; started_at?: number; + pending_prompt?: string; events?: ClawRunEvent[]; }; @@ -502,6 +503,32 @@ export function toReplayRepository( previousId = id; } if (isActiveRunStatus(runStatus)) { + const pendingPrompt = resolvePendingPrompt(replayMessages, runStatus); + if (pendingPrompt) { + const id = `${fallbackSessionId}-pending-${runStatus.run_id ?? "active"}`; + const parts = [ + { type: "text", text: pendingPrompt }, + ] as UIMessage["parts"]; + const uiMessage: UIMessage = { + id, + role: "user", + parts, + metadata: { sessionId: session.session_id ?? fallbackSessionId }, + } as UIMessage; + const threadMessage = { + id, + role: "user", + createdAt: new Date(), + content: toThreadMessageContent(parts), + attachments: [], + metadata: { + custom: { sessionId: session.session_id ?? fallbackSessionId }, + }, + } as ThreadMessage; + bindExternalStoreMessage(threadMessage, uiMessage); + messages.push({ message: threadMessage, parentId: previousId }); + previousId = id; + } const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`; const runStartedAtMs = resolveRunStartedAtMs(runStatus); const parts = buildActiveRunParts(runStatus) as UIMessage["parts"]; @@ -717,6 +744,29 @@ function isRunStatusMessage(message?: ClawStoredMessage) { return message?.metadata?.kind === "run_status"; } +function resolvePendingPrompt( + messages: readonly ClawStoredMessage[], + runStatus: ClawRunStatus, +) { + const pendingPrompt = + typeof runStatus.pending_prompt === "string" + ? 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; + const content = cleanStoredContent(message.content ?? "").trim(); + if (!content || content.trimStart().startsWith("")) + continue; + return message.role === "user" && content === pendingPrompt + ? null + : pendingPrompt; + } + return pendingPrompt; +} + function normalizeRunTimestampMs(value: unknown) { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value);