turns: survive the surface disappearing
This commit is contained in:
parent
783f09606a
commit
3aa7019eb5
8 changed files with 426 additions and 168 deletions
|
|
@ -133,6 +133,16 @@ Singleton {
|
|||
}
|
||||
|
||||
function onStreamEvent(event) {
|
||||
// A reattached stream has no local message object: the old one
|
||||
// died with the shell. Create it lazily on the first event that
|
||||
// actually belongs in the assistant bubble; idle resume produces
|
||||
// no empty card.
|
||||
if (!root.streamingMessage && [
|
||||
"assistant_message", "reasoning_message", "tool_call_message",
|
||||
"tool_return_message", "interstitial"
|
||||
].indexOf(event.message_type) >= 0) {
|
||||
root._startStreaming();
|
||||
}
|
||||
root.handleStreamEvent(event);
|
||||
}
|
||||
|
||||
|
|
@ -594,23 +604,40 @@ Singleton {
|
|||
root.tokenCount.total = -1;
|
||||
|
||||
messages.forEach(message => {
|
||||
const segments = (message.blocks ?? []).map(block => {
|
||||
const segments = [];
|
||||
(message.blocks ?? []).forEach(block => {
|
||||
switch (block.type) {
|
||||
case "text": return { type: "text", content: block.text ?? "" };
|
||||
case "reasoning": return { type: "think", content: block.reasoning ?? "" };
|
||||
case "tool_use": return {
|
||||
type: "tool", id: block.id ?? "", name: block.name ?? "tool",
|
||||
arguments: block.input ?? "", round: 0, status: "done", output: "", failed: false,
|
||||
};
|
||||
case "tool_result": return {
|
||||
type: "tool", id: block.tool_use_id ?? "", name: block.tool_name ?? "tool",
|
||||
arguments: "", round: 0, status: block.is_error ? "error" : "done",
|
||||
output: block.output ?? "", failed: block.is_error ?? false,
|
||||
};
|
||||
case "image": return { type: "text", content: "[image]" };
|
||||
default: return null;
|
||||
case "text":
|
||||
segments.push({ type: "text", content: block.text ?? "" });
|
||||
break;
|
||||
case "reasoning":
|
||||
segments.push({ type: "think", content: block.reasoning ?? "" });
|
||||
break;
|
||||
case "tool_use":
|
||||
segments.push({
|
||||
type: "tool", id: block.id ?? "", name: block.name ?? "tool",
|
||||
arguments: block.input ?? "", round: 0, status: "running", output: "", failed: false,
|
||||
});
|
||||
break;
|
||||
case "tool_result": {
|
||||
const resultId = String(block.tool_use_id ?? "");
|
||||
const index = segments.findIndex(segment =>
|
||||
segment.type === "tool" && segment.id === resultId);
|
||||
const result = {
|
||||
type: "tool", id: resultId, name: block.tool_name ?? "tool",
|
||||
arguments: index >= 0 ? segments[index].arguments : "", round: 0,
|
||||
status: block.is_error ? "error" : "done",
|
||||
output: block.output ?? "", failed: block.is_error ?? false,
|
||||
};
|
||||
if (index >= 0) segments[index] = result;
|
||||
else segments.push(result);
|
||||
break;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
case "image":
|
||||
segments.push({ type: "text", content: "[image]" });
|
||||
break;
|
||||
}
|
||||
});
|
||||
const content = segments.map(segment => {
|
||||
if (segment.type === "tool") return `${segment.name}(${segment.arguments})\n${segment.output}`;
|
||||
return segment.content;
|
||||
|
|
|
|||
|
|
@ -272,6 +272,11 @@ Singleton {
|
|||
const messages = JSON.parse(text);
|
||||
root.conversationId = loadConversation.requestedConversationId;
|
||||
root.conversationResumed(root.currentAgentId, root.conversationId, messages);
|
||||
// If the shell reloaded in the middle of a turn, the POST
|
||||
// socket that started it is gone but the turn belongs to
|
||||
// the server and is still running. Replay its journal and
|
||||
// follow the live tail without posting another message.
|
||||
Qt.callLater(root._reattachActiveTurn);
|
||||
} catch (e) {
|
||||
console.log("[Souveraine] Could not parse conversation transcript:", e);
|
||||
}
|
||||
|
|
@ -309,6 +314,49 @@ Singleton {
|
|||
loadConversation.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: reattachRequester
|
||||
property bool receivedEvent: false
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
if (data.length === 0 || !data.startsWith("data:")) return;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(data.slice(5).trim());
|
||||
} catch (e) {
|
||||
console.log("[Souveraine] Unparseable replay SSE line:", data);
|
||||
return;
|
||||
}
|
||||
reattachRequester.receivedEvent = true;
|
||||
root.streamEvent(event);
|
||||
}
|
||||
}
|
||||
onExited: exitCode => {
|
||||
if (root.turnStartedAt > 0)
|
||||
root.turnElapsedMs = Date.now() - root.turnStartedAt;
|
||||
root.turnActive = false;
|
||||
// A 409 means there was no turn to recover. It is an ordinary
|
||||
// resume, not a failed response and must not finish a blank card.
|
||||
if (reattachRequester.receivedEvent)
|
||||
root.streamClosed(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
function _reattachActiveTurn() {
|
||||
if (root.conversationId.length === 0 || requester.running || reattachRequester.running)
|
||||
return;
|
||||
reattachRequester.receivedEvent = false;
|
||||
reattachRequester.command = ["bash", "-c",
|
||||
root._tokenReadLine(root.currentAgentId)
|
||||
+ `curl --no-buffer -sf "${root.serverBase}/v1/conversations/${root.conversationId}/events"`
|
||||
+ ` -H "Authorization: Bearer $TOKEN"`
|
||||
];
|
||||
root.turnStartedAt = Date.now();
|
||||
root.turnElapsedMs = 0;
|
||||
root.turnActive = true;
|
||||
reattachRequester.running = true;
|
||||
}
|
||||
|
||||
// ── Ambient sensorium ────────────────────────────────────────────────
|
||||
// What the desktop feels like at the moment of speaking. Cheap,
|
||||
// synchronous reads here; the cursor needs a hyprctl round-trip and is
|
||||
|
|
|
|||
Loading…
Reference in a new issue