recover still-running background tasks on reattach
A task row is officer's own invention, synthesised from the harness's system.task_started, and nothing corresponding to it is ever written to Claude's transcript. So rebuildTranscript can only produce user/tool/assistant rows, and sync:live deliberately carries no messages — which left the background-task tray empty after a mid-task refresh even though the work was still running. Fold the durable log on attach into started-minus-notified and hand that back on sync:live. The same read now supplies the cursor, so this costs one query rather than two. Finished tasks are excluded: replaying those would resurrect rows already seen to resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,8 @@ export type ChatMessage =
|
||||
done?: boolean;
|
||||
};
|
||||
|
||||
export type RunningTask = { taskId: string; description: string; taskType?: string };
|
||||
|
||||
export type TaskInfo = {
|
||||
taskName: string;
|
||||
taskDirName: string;
|
||||
@@ -105,7 +107,20 @@ export type ServerMessage =
|
||||
* transcript came over HTTP a moment ago and there is no shared id to reconcile the two records by, so
|
||||
* this hands over the rest of the turn and the half-written paragraph, and nothing that would double up.
|
||||
*/
|
||||
| { type: 'sync:live'; sessionId: string; isGenerating: boolean; streamingText: string; cursor: number }
|
||||
| {
|
||||
type: 'sync:live';
|
||||
sessionId: string;
|
||||
isGenerating: boolean;
|
||||
streamingText: string;
|
||||
cursor: number;
|
||||
/**
|
||||
* Background tasks started and not yet finished. The single exception to "carries no messages": a
|
||||
* task row has no counterpart in Claude's transcript — officer synthesises it — so rebuilding from
|
||||
* the file cannot bring one back, and the tray came up empty on a refresh while work was still
|
||||
* running. Finished tasks are excluded server-side.
|
||||
*/
|
||||
runningTasks: RunningTask[];
|
||||
}
|
||||
| { type: 'error'; message: string; errorCode?: string }
|
||||
| { type: 'stopped' }
|
||||
/** The agent process went away mid-turn. Recoverable — see the `cutoff` message role. */
|
||||
|
||||
@@ -355,6 +355,19 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
streamingRef.current = msg.streamingText;
|
||||
flushStreaming();
|
||||
}
|
||||
// The one thing the transcript genuinely cannot supply. Appended rather than spliced into place:
|
||||
// a task row carries no timestamp to sort by, so a recovered one lands at the end of the
|
||||
// conversation instead of where it was started. The tray reads by taskId and renders correctly
|
||||
// either way, and the alternative is inventing an ordering the record does not contain.
|
||||
if (msg.runningTasks?.length) {
|
||||
setMessages((prev) => {
|
||||
const known = new Set(prev.filter((m) => m.role === 'task').map((m) => m.taskId));
|
||||
const recovered = msg.runningTasks
|
||||
.filter((t) => !known.has(t.taskId))
|
||||
.map((t): ChatMessage => ({ role: 'task', ...t }));
|
||||
return recovered.length ? [...prev, ...recovered] : prev;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user