Watch
1
0
Fork
You've already forked souveraine
0

turns: survive the surface disappearing

This commit is contained in:
Fimeg 2026-08-11 15:01:18 -04:00
commit 3aa7019eb5
8 changed files with 426 additions and 168 deletions

View file

@ -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;

View file

@ -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