fix(chat): handle raw structured session NOT_FOUND errors from Tauri

Elargit isNotFound() pour reconnaitre aussi la forme brute
'not found: structured session <uuid>' que Tauri peut retourner,
en plus de la forme typée {code: 'NOT_FOUND'}.

Ajoute deux tests couvrant:
- fallback sur reattach avec erreur brute
- retry prompt sur send avec erreur brute

QA: tests passés sur cette correction.
This commit is contained in:
2026-08-05 16:00:37 +02:00
parent 62ac05a080
commit 7071c53bb2
5 changed files with 168 additions and 7 deletions

View File

@ -1,8 +1,8 @@
---
issueRef: "#149"
version: 14
version: 15
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedAt: 1785934744273
updatedAt: 1785937428431
---
# Historique des tentatives
@ -13,7 +13,7 @@ updatedAt: 1785934744273
- Action Main: reouverture du ticket et relance d'un cycle complet Architect -> Git -> DevFrontend -> QA.
## Piste precedemment documentee dans la description
- Hypothese precedente: fallback premature dans `frontend/src/features/layout/LayoutGrid.tsx` pendant le chargement asynchrone initial du catalogue agent/profil, ecrasant `cellMode=custom` restaure depuis le storage.
- Hypothese precedente: fallback premature dans `frontend/src/features/layout/LayoutGrid.tsx` pendant le chargement asynchrone initial du catalogue agent/profil, ecrasant `cellMode="custom"` restaure depuis le storage.
- Cette piste n'a pas suffi a eliminer le symptome utilisateur, donc elle doit etre revalidee ou completee avant nouvelle correction.
## 2026-08-05 — Recadrage Architecture (avant nouvelle implementation)
@ -124,3 +124,28 @@ updatedAt: 1785934744273
2. rebuild/reinstall AppImage depuis la branche contenant `ce9ba0dc`, relancer IdeA, puis retester ;
3. si la cellule retombe encore vers Plain/TUI, capturer les logs `[ticket149]` au moment exact du repro et comparer: `openOrAttach:self-session-echo:skip`, `vm.setSession`, `shouldFallbackCustomCliMode`, `customCliAvailable`, `effectiveCustomCliAvailable`, `trustedCustomCli`;
4. si les logs montrent que `openOrAttach` reste stable, la prochaine hypothese doit sortir de `CustomAgentChatView` et porter sur la condition de rendu `customCliAgent/customCliProfile/effectiveCustomCliAvailable` ou sur un evenement externe qui remplace le mode/vue, avec trace runtime avant patch.
## 2026-08-05 — Nouveau symptome apres le fix session echo
- Retour utilisateur recu apres `ce9ba0dc`: la CLI custom peut maintenant se lancer, mais l'UI affiche l'erreur exacte `not found: structured session 69b57638-0ac7-4a79-8252-e0436a3265f6`.
- Changement de symptome important: on n'est plus sur la retombee immediate vers Plain/TUI telle que documentee plus haut ; on est maintenant sur un echec de rattachement/reutilisation d'une session structuree.
- Verification statique Main (sans nouvelle implementation a ce stade):
1. `frontend/src/features/agents/CustomAgentChatView.tsx` est cense **absorber** `NOT_FOUND` sur `reattachAgentChat` puis relancer une session fraiche, y compris pour un `sessionId` stale restaure depuis le parent ; les tests couvrent deja ce cas.
2. Le meme composant sait aussi recuperer d'un premier `launchAgentChat` qui renvoie un `sessionId` mort, a condition qu'un second launch/reattach reussisse ; ce cas est aussi teste.
3. Voir quand meme cette erreur dans l'UI suggere donc plutot l'un de ces ecarts restants:
- l'erreur reelle renvoyee par `invoke(...)` n'est pas typée `code: "NOT_FOUND"` cote frontend, donc `isNotFound(...)` ne la reconnait pas et elle remonte telle quelle ;
- ou bien `launch_agent` renvoie lui-meme un `sessionId` structure qui n'est deja plus present dans `structured_sessions` au moment du `reattach`, potentiellement plus d'une fois, ce qui epuise la logique de retry et surface finalement `not found: structured session ...`.
- Ownership provisoire pour la reprise:
- piste prioritaire **frontend/runtime contract**: confirmer la forme exacte de l'erreur recue par `CustomAgentChatView` (`code`, `message`, `name`, `raw`) dans les logs `[ticket149]` deja poses ;
- piste secondaire **backend structured registry**: si `code === "NOT_FOUND"` est bien present sur un `sessionId` fraichement renvoye par `launch_agent`, alors l'anomalie bascule cote creation/enregistrement/liveness de `structured_sessions`.
- Commandes de lecture executees par Main pour ce recadrage:
1. `git status --short --branch` -> branche courante `feature/ticket149-customchat-session-instrumentation`, avec un changement non lie `.ideai/idea-android-plugin.json` deja present ;
2. `git log --oneline --decorate -n 12` -> HEAD `62ac05a0` au-dessus de `ce9ba0dc` ;
3. lecture de `frontend/src/features/agents/CustomAgentChatView.tsx`, de ses tests, et de `crates/app-tauri/src/commands.rs` autour de `reattach_agent_chat` / `launch_agent`.
- Blocage de pilotage dans ce tour:
- `idea_ask_agent(Architect)` et `idea_ask_agent(Git)` ont tous deux repondu `You've hit your session limit · resets 4:50pm (Europe/Paris)` ; la delegation specialisee est donc temporairement indisponible jusqu'au **2026-08-05 16:50 Europe/Paris**.
- Etape suivante imposee a la reprise apres 16:50:
1. refaire la delegation `Architect` pour trancher frontend vs backend sur ce nouveau symptome ;
2. faire trancher `Git` sur la branche de travail a conserver/reprendre ;
3. envoyer a `DevFrontend` une tache d'instrumentation/verification du contrat d'erreur si `code` manque ;
4. envoyer a `DevBackend` une tache de verification de l'enregistrement/liveness `structured_sessions` si un `sessionId` fraichement lance ressort deja en `NOT_FOUND` ;
5. faire valider par `QA` sur repro reel avec le message exact et, si possible, les logs `[ticket149]` au moment du repro.

View File

@ -11,8 +11,8 @@ attachments: []
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
createdAt: 1785930073255
updatedAt: 1785934744273
version: 14
updatedAt: 1785937428431
version: 15
---
Bug report utilisateur du 2026-08-05: la CLI custom ne se lance plus; à l'ouverture elle se ferme immédiatement et la cellule revient sur Plain/TUI. Diagnostic Architecture: bug distinct de #148. Dans `frontend/src/features/layout/LayoutGrid.tsx`, `cellMode` restauré à `custom` depuis le storage est forcé trop tôt vers `tui` par l'effet garde-fou `if (!customCliAvailable && cellMode !== \"tui\") setCellMode(\"tui\")`, alors que `agents`/`profiles` sont encore vides pendant leur chargement asynchrone initial. Résultat: la préférence `custom` est écrasée silencieusement avant même que la vue puisse se monter. Objectif: retarder ce fallback jusqu'à la fin du premier chargement du catalogue, couvrir par test de non-régression, valider QA, puis rebuild AppImage Linux.

View File

@ -1960,7 +1960,7 @@
"kind": "agent",
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
},
"updatedAt": 1785934744273
"updatedAt": 1785937428431
}
]
}

View File

@ -152,6 +152,65 @@ describe("CustomAgentChatView", () => {
expect(screen.queryByText(/structured session gone/)).toBeNull();
});
it("falls back when Tauri surfaces structured session NOT_FOUND as a raw string", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
sessionId: "chat-session-2",
assignedConversationId: "conversation-2",
})),
reattachAgentChat: vi
.fn()
.mockRejectedValueOnce(
"not found: structured session 69b57638-0ac7-4a79-8252-e0436a3265f6",
)
.mockImplementationOnce(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const onSessionId = vi.fn();
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="69b57638-0ac7-4a79-8252-e0436a3265f6"
conversationId="conversation-1"
onSessionId={onSessionId}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledTimes(1));
expect(agent.reattachAgentChat).toHaveBeenNthCalledWith(
1,
"69b57638-0ac7-4a79-8252-e0436a3265f6",
expect.any(Function),
);
expect(agent.reattachAgentChat).toHaveBeenNthCalledWith(
2,
"chat-session-2",
expect.any(Function),
);
expect(onSessionId).toHaveBeenCalledWith(null);
expect(onSessionId).toHaveBeenCalledWith("chat-session-2");
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("does not reopen when the parent echoes a launched session id", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
@ -286,6 +345,74 @@ describe("CustomAgentChatView", () => {
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("recovers and retries the prompt when send reports structured session NOT_FOUND as a raw string", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
sessionId: "chat-session-2",
assignedConversationId: "conversation-2",
})),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi
.fn()
.mockRejectedValueOnce("not found: structured session stale-session")
.mockImplementationOnce(async (_sessionId: string, _prompt: string, onChunk) => {
onChunk({ kind: "final", content: "Recovered reply" });
}),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const onSessionId = vi.fn();
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="stale-session"
conversationId="conversation-1"
onSessionId={onSessionId}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() =>
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
"stale-session",
expect.any(Function),
),
);
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "please recover" },
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2));
expect(agent.sendAgentChat).toHaveBeenNthCalledWith(
2,
"chat-session-2",
"please recover",
expect.any(Function),
);
expect(onSessionId).toHaveBeenCalledWith(null);
expect(onSessionId).toHaveBeenCalledWith("chat-session-2");
expect(await screen.findByText("Recovered reply")).toBeTruthy();
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("treats NOT_FOUND during cancel as an already-gone session", async () => {
const agent = {
launchAgentChat: vi.fn(),

View File

@ -54,7 +54,16 @@ function instrumentationError(e: unknown): Record<string, unknown> {
}
function isNotFound(e: unknown): boolean {
return Boolean(e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND");
if (e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND") {
return true;
}
const message =
typeof e === "string"
? e
: e && typeof e === "object" && "message" in e
? String((e as GatewayError).message)
: "";
return /^not found: structured session\b/i.test(message);
}
function unknownChunkLabel(chunk: unknown): string {