fix: keep the conversation when the agent list is re-polled
selectAgent cleared conversationId even when the agent hadn't changed, and the 60s inventory poll re-selects the persisted agent forever — so any message sent a minute after the last one opened a fresh conversation. 27 of them yesterday, four messages each. An empty curl body no longer counts as "no conversations".
This commit is contained in:
parent
e4d6ddcc72
commit
e4e659483b
1 changed files with 65 additions and 10 deletions
|
|
@ -44,6 +44,19 @@ Singleton {
|
|||
property var agents: ({})
|
||||
property var agentList: Object.keys(agents)
|
||||
property string currentAgentId: ""
|
||||
// Auto-resume: whenever the active agent is (re)established — shell start,
|
||||
// reboot, agent switch — re-attach her latest server-persisted conversation
|
||||
// so the next message continues the thread instead of minting a fresh one.
|
||||
// Without this every shell reload starts amnesiac, and the shell reloads
|
||||
// often (a deploy, a lock, a crash). The server is the source of truth here;
|
||||
// the surface deliberately keeps no agent → conversation map of its own.
|
||||
// Gated so it never clobbers a live turn or an already-attached thread.
|
||||
onCurrentAgentIdChanged: {
|
||||
if (root.currentAgentId.length > 0 && root.serverUp
|
||||
&& root.conversationId.length === 0 && !root.turnActive) {
|
||||
root.resumeLatestConversation();
|
||||
}
|
||||
}
|
||||
property string conversationId: ""
|
||||
property bool turnActive: false
|
||||
|
||||
|
|
@ -163,11 +176,30 @@ Singleton {
|
|||
|
||||
function selectAgent(agentId) {
|
||||
if (!root.agents[agentId]) return false;
|
||||
// Re-affirming the agent that is already active is a no-op, and it has
|
||||
// to be. Ai.qml re-selects the persisted agent on every agentsRefreshed,
|
||||
// and the inventory poll above fires that once a minute, forever. While
|
||||
// this function cleared conversationId unconditionally, every message
|
||||
// sent more than a minute after the previous one opened a NEW
|
||||
// conversation and therefore arrived with no history at all.
|
||||
//
|
||||
// Measured on the phone 2026-07-31: 27 conversations for one agent in a
|
||||
// day, all but two exactly four messages long — system prompt, ambient,
|
||||
// user, reply. One exchange each. That is the whole of the "she doesn't
|
||||
// remember what I just said" report, and it is not the turn loop: the
|
||||
// server assembles history from session.messages correctly, and there
|
||||
// was simply never more than one exchange in a session to assemble.
|
||||
//
|
||||
// Switching agents is a decision. Polling is not.
|
||||
if (agentId === root.currentAgentId) return true;
|
||||
// A live turn belongs to the current conversation. Switching beneath
|
||||
// it would render one agent's response in another agent's surface.
|
||||
if (root.turnActive) return false;
|
||||
root.currentAgentId = agentId;
|
||||
// Clear before the id changes, so onCurrentAgentIdChanged observes an
|
||||
// empty conversation and re-attaches the incoming agent's own latest
|
||||
// thread rather than leaving her on a blank one.
|
||||
root.conversationId = "";
|
||||
root.currentAgentId = agentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -184,25 +216,40 @@ Singleton {
|
|||
property string agentId: ""
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (listConversations.agentId !== root.currentAgentId) return;
|
||||
// Empty stdout is a FAILED request, not an empty agent: curl -sf
|
||||
// writes nothing on 4xx/5xx. Conflating the two clears
|
||||
// conversationId and hands the surface an empty transcript to
|
||||
// render, so one transient hiccup wipes the visible thread and
|
||||
// orphans the live one. Only a parsed response is authoritative.
|
||||
if (text.length === 0) {
|
||||
console.log("[Souveraine] empty conversation list response — leaving current conversation in place");
|
||||
return;
|
||||
}
|
||||
let conversations = [];
|
||||
try {
|
||||
conversations = text.length > 0 ? JSON.parse(text) : [];
|
||||
conversations = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.log("[Souveraine] Could not parse conversation list:", e);
|
||||
return;
|
||||
}
|
||||
if (listConversations.agentId !== root.currentAgentId) return;
|
||||
if (conversations.length === 0) {
|
||||
// The server answered, and the answer is "none yet".
|
||||
root.conversationId = "";
|
||||
root.conversationResumed(root.currentAgentId, "", []);
|
||||
return;
|
||||
}
|
||||
// Server sorts by updated_at descending — [0] is her latest.
|
||||
root._loadConversation(listConversations.agentId, conversations[0].id);
|
||||
}
|
||||
}
|
||||
onExited: exitCode => {
|
||||
// A failed list fetch is transient (server busy, network blip).
|
||||
// Do NOT clear conversationId — that orphans the live thread and
|
||||
// forces the next send to mint a fresh conversation. Log and leave
|
||||
// state alone; the next refresh or /resume retries.
|
||||
if (exitCode !== 0 && listConversations.agentId === root.currentAgentId) {
|
||||
root.conversationId = "";
|
||||
root.conversationResumed(root.currentAgentId, "", []);
|
||||
console.log("[Souveraine] conversation list fetch failed (exit " + exitCode + ") — leaving current conversation in place");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,21 +260,29 @@ Singleton {
|
|||
property string requestedConversationId: ""
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (loadConversation.agentId !== root.currentAgentId) return;
|
||||
// Same rule as the list above: no body means the fetch failed.
|
||||
// Attaching to the id anyway and announcing an empty transcript
|
||||
// would blank the surface while claiming the thread is loaded.
|
||||
if (text.length === 0) {
|
||||
console.log("[Souveraine] empty transcript response — leaving current conversation in place");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const messages = text.length > 0 ? JSON.parse(text) : [];
|
||||
if (loadConversation.agentId !== root.currentAgentId) return;
|
||||
const messages = JSON.parse(text);
|
||||
root.conversationId = loadConversation.requestedConversationId;
|
||||
root.conversationResumed(root.currentAgentId, root.conversationId, messages);
|
||||
} catch (e) {
|
||||
console.log("[Souveraine] Could not parse conversation transcript:", e);
|
||||
root.conversationResumed(root.currentAgentId, "", []);
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: exitCode => {
|
||||
// Transient failure (e.g. a 5xx on GET /messages). Don't clear —
|
||||
// see listConversations.onExited. Leaving conversationId alone keeps
|
||||
// an already-attached thread reachable instead of forcing a new one.
|
||||
if (exitCode !== 0 && loadConversation.agentId === root.currentAgentId) {
|
||||
root.conversationId = "";
|
||||
root.conversationResumed(root.currentAgentId, "", []);
|
||||
console.log("[Souveraine] conversation transcript fetch failed (exit " + exitCode + ") — leaving current conversation in place");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue