chore(chat): instrumentation session CustomAgentChatView pour diagnostic #149

Ajoute des points de traçage ciblés sur le cycle de vie de session
CustomAgentChatView/LayoutGrid pour isoler la cause du fallback
silencieux CLI custom avant tentative de fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 15:23:45 +02:00
parent 4d8b69afca
commit 029ed97bff
2 changed files with 149 additions and 20 deletions

View File

@ -40,6 +40,19 @@ function describe(e: unknown): string {
return String(e); return String(e);
} }
function instrumentationError(e: unknown): Record<string, unknown> {
if (e && typeof e === "object") {
const err = e as GatewayError & { name?: string };
return {
code: err.code,
message: err.message,
name: err.name,
raw: e,
};
}
return { message: String(e), raw: e };
}
function isNotFound(e: unknown): boolean { function isNotFound(e: unknown): boolean {
return Boolean(e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND"); return Boolean(e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND");
} }
@ -127,6 +140,7 @@ export function CustomAgentChatView({
const sessionRef = useRef<string | null>(sessionId); const sessionRef = useRef<string | null>(sessionId);
sessionRef.current = currentSession; sessionRef.current = currentSession;
const mountedRef = useRef(false); const mountedRef = useRef(false);
const openOrAttachCountRef = useRef(0);
const onSessionIdRef = useRef(onSessionId); const onSessionIdRef = useRef(onSessionId);
onSessionIdRef.current = onSessionId; onSessionIdRef.current = onSessionId;
const onConversationIdRef = useRef(onConversationId); const onConversationIdRef = useRef(onConversationId);
@ -156,7 +170,28 @@ export function CustomAgentChatView({
const reattachStructuredSession = useCallback( const reattachStructuredSession = useCallback(
async (sid: string, options: { applyScrollback?: boolean } = {}) => { async (sid: string, options: { applyScrollback?: boolean } = {}) => {
if (!agent.reattachAgentChat) throw new Error("Structured reattach unavailable"); if (!agent.reattachAgentChat) throw new Error("Structured reattach unavailable");
const reattached = await agent.reattachAgentChat(sid, receive); console.debug("[ticket149] reattachStructuredSession:start", {
timestamp: new Date().toISOString(),
sessionId: sid,
options,
});
let reattached: Awaited<ReturnType<NonNullable<typeof agent.reattachAgentChat>>>;
try {
reattached = await agent.reattachAgentChat(sid, receive);
} catch (e) {
console.error("[ticket149] reattachStructuredSession:error", {
timestamp: new Date().toISOString(),
sessionId: sid,
error: instrumentationError(e),
});
throw e;
}
console.debug("[ticket149] reattachStructuredSession:success", {
timestamp: new Date().toISOString(),
requestedSessionId: sid,
returnedSessionId: reattached.sessionId,
scrollbackCount: reattached.scrollback.length,
});
if (!mountedRef.current) return reattached.sessionId; if (!mountedRef.current) return reattached.sessionId;
setCurrentSession(reattached.sessionId); setCurrentSession(reattached.sessionId);
if (options.applyScrollback) { if (options.applyScrollback) {
@ -170,14 +205,47 @@ export function CustomAgentChatView({
const recoverStructuredSession = useCallback( const recoverStructuredSession = useCallback(
async (options: { applyScrollback?: boolean; retryAttachNotFound?: boolean } = {}) => { async (options: { applyScrollback?: boolean; retryAttachNotFound?: boolean } = {}) => {
if (!agent.launchAgentChat) throw new Error("Structured launch unavailable"); if (!agent.launchAgentChat) throw new Error("Structured launch unavailable");
console.debug("[ticket149] recoverStructuredSession:start", {
timestamp: new Date().toISOString(),
currentSession: sessionRef.current,
conversationId,
options,
});
let retryAttachNotFound = Boolean(options.retryAttachNotFound); let retryAttachNotFound = Boolean(options.retryAttachNotFound);
for (;;) { for (;;) {
const launched = await agent.launchAgentChat(projectId, agentId, { const launchRequest = {
cwd, cwd,
rows: 24, rows: 24,
cols: 80, cols: 80,
conversationId: conversationId ?? undefined, conversationId: conversationId ?? undefined,
nodeId, nodeId,
};
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:start", {
timestamp: new Date().toISOString(),
projectId,
agentId,
request: launchRequest,
options,
retryAttachNotFound,
});
let launched: Awaited<ReturnType<NonNullable<typeof agent.launchAgentChat>>>;
try {
launched = await agent.launchAgentChat(projectId, agentId, launchRequest);
} catch (e) {
console.error("[ticket149] recoverStructuredSession:launchAgentChat:error", {
timestamp: new Date().toISOString(),
projectId,
agentId,
request: launchRequest,
error: instrumentationError(e),
});
throw e;
}
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:success", {
timestamp: new Date().toISOString(),
projectId,
agentId,
launched,
}); });
if (!mountedRef.current) return launched.sessionId; if (!mountedRef.current) return launched.sessionId;
setCurrentSession(launched.sessionId); setCurrentSession(launched.sessionId);
@ -186,9 +254,15 @@ export function CustomAgentChatView({
onConversationIdRef.current(launched.assignedConversationId); onConversationIdRef.current(launched.assignedConversationId);
} }
try { try {
return await reattachStructuredSession(launched.sessionId, { const recoveredSessionId = await reattachStructuredSession(launched.sessionId, {
applyScrollback: options.applyScrollback, applyScrollback: options.applyScrollback,
}); });
console.debug("[ticket149] recoverStructuredSession:success", {
timestamp: new Date().toISOString(),
launchedSessionId: launched.sessionId,
recoveredSessionId,
});
return recoveredSessionId;
} catch (e) { } catch (e) {
if (isNotFound(e) && retryAttachNotFound) { if (isNotFound(e) && retryAttachNotFound) {
retryAttachNotFound = false; retryAttachNotFound = false;
@ -198,6 +272,11 @@ export function CustomAgentChatView({
} }
continue; continue;
} }
console.error("[ticket149] recoverStructuredSession:reattach:error", {
timestamp: new Date().toISOString(),
launchedSessionId: launched.sessionId,
error: instrumentationError(e),
});
throw e; throw e;
} }
} }
@ -223,6 +302,13 @@ export function CustomAgentChatView({
let cancelled = false; let cancelled = false;
async function openOrAttach() { async function openOrAttach() {
const execution = openOrAttachCountRef.current + 1;
openOrAttachCountRef.current = execution;
console.debug("[ticket149] openOrAttach:start", {
timestamp: new Date().toISOString(),
execution,
sessionId,
});
setOpening(true); setOpening(true);
setError(null); setError(null);
try { try {
@ -230,6 +316,11 @@ export function CustomAgentChatView({
try { try {
await reattachStructuredSession(sessionId, { applyScrollback: true }); await reattachStructuredSession(sessionId, { applyScrollback: true });
if (cancelled) return; if (cancelled) return;
console.debug("[ticket149] openOrAttach:reattach-existing:success", {
timestamp: new Date().toISOString(),
execution,
sessionId,
});
return; return;
} catch (e) { } catch (e) {
// The backend contract for `reattach_agent_chat` (NOT_FOUND) is that // The backend contract for `reattach_agent_chat` (NOT_FOUND) is that
@ -240,6 +331,12 @@ export function CustomAgentChatView({
setCurrentSession(null); setCurrentSession(null);
onSessionIdRef.current(null); onSessionIdRef.current(null);
} }
console.debug("[ticket149] openOrAttach:reattach-existing:not-found", {
timestamp: new Date().toISOString(),
execution,
sessionId,
error: instrumentationError(e),
});
} }
} }
await recoverStructuredSession({ await recoverStructuredSession({
@ -247,7 +344,18 @@ export function CustomAgentChatView({
retryAttachNotFound: true, retryAttachNotFound: true,
}); });
if (cancelled) return; if (cancelled) return;
console.debug("[ticket149] openOrAttach:recover:success", {
timestamp: new Date().toISOString(),
execution,
receivedSessionId: sessionId,
});
} catch (e) { } catch (e) {
console.error("[ticket149] openOrAttach:error", {
timestamp: new Date().toISOString(),
execution,
sessionId,
error: instrumentationError(e),
});
if (!cancelled) setError(describe(e)); if (!cancelled) setError(describe(e));
} finally { } finally {
if (!cancelled) setOpening(false); if (!cancelled) setOpening(false);

View File

@ -624,19 +624,24 @@ function LeafView({
legitChange?.agentId === pinnedAgent.id && legitChange?.agentId === pinnedAgent.id &&
legitChange.profileId === pinnedAgent.profileId, legitChange.profileId === pinnedAgent.profileId,
); );
if ( const fallbackInput = {
shouldFallbackCustomCliMode({ agentsLoaded,
agentsLoaded, profilesLoaded,
profilesLoaded, cellMode,
cellMode, hasPinnedAgent: Boolean(pinnedAgent),
hasPinnedAgent: Boolean(pinnedAgent), hasPinnedProfile: Boolean(pinnedProfile),
hasPinnedProfile: Boolean(pinnedProfile), customCliAvailable,
customCliAvailable, effectiveCustomCliAvailable,
effectiveCustomCliAvailable, hasTrustedCustomCli: Boolean(trustedCustomCli),
hasTrustedCustomCli: Boolean(trustedCustomCli), hasLegitProfileChange: hasLegitDowngrade,
hasLegitProfileChange: hasLegitDowngrade, };
}) const shouldFallback = shouldFallbackCustomCliMode(fallbackInput);
) { console.debug("[ticket149] shouldFallbackCustomCliMode", {
timestamp: new Date().toISOString(),
input: fallbackInput,
result: shouldFallback,
});
if (shouldFallback) {
setTrustedCustomCli(null); setTrustedCustomCli(null);
setCellMode("tui"); setCellMode("tui");
} }
@ -693,6 +698,22 @@ function LeafView({
else setCellMode(target); else setCellMode(target);
} }
async function setSessionWithInstrumentation(
site: "custom" | "plain" | "plain-background-attach" | "mode-switch",
nextSession: string | null,
): Promise<void> {
console.debug("[ticket149] vm.setSession", {
timestamp: new Date().toISOString(),
nodeId: id,
site,
previousSession: session,
nextSession,
cellMode,
agentId,
});
await vm.setSession(id, nextSession);
}
async function stopCurrentSessionForSwitch(): Promise<void> { async function stopCurrentSessionForSwitch(): Promise<void> {
if (!session) return; if (!session) return;
if (agentId && agentGateway?.stopLiveAgent) { if (agentId && agentGateway?.stopLiveAgent) {
@ -708,7 +729,7 @@ function LeafView({
} else { } else {
await terminal?.closeTerminal(session); await terminal?.closeTerminal(session);
} }
await vm.setSession(id, null); await setSessionWithInstrumentation("mode-switch", null);
refreshLive(); refreshLive();
} }
@ -863,7 +884,7 @@ function LeafView({
if (background?.sessionId && agentGateway!.attachLiveAgent) { if (background?.sessionId && agentGateway!.attachLiveAgent) {
const attached = await agentGateway!.attachLiveAgent(projectId, agentId!, id); const attached = await agentGateway!.attachLiveAgent(projectId, agentId!, id);
const sessionId = attached.sessionId ?? background.sessionId; const sessionId = attached.sessionId ?? background.sessionId;
void vm.setSession(id, sessionId); void setSessionWithInstrumentation("plain-background-attach", sessionId);
const result = await agentGateway!.reattach(sessionId, onData); const result = await agentGateway!.reattach(sessionId, onData);
refreshLive(); refreshLive();
return result.handle; return result.handle;
@ -1299,7 +1320,7 @@ function LeafView({
nodeId={id} nodeId={id}
sessionId={session} sessionId={session}
conversationId={conversationId} conversationId={conversationId}
onSessionId={(sid) => void vm.setSession(id, sid)} onSessionId={(sid) => void setSessionWithInstrumentation("custom", sid)}
onConversationId={(cid) => void vm.setCellConversation(id, cid)} onConversationId={(cid) => void vm.setCellConversation(id, cid)}
/> />
) : ( ) : (
@ -1309,7 +1330,7 @@ function LeafView({
open={terminalOpener} open={terminalOpener}
reattach={reattachOpener} reattach={reattachOpener}
sessionId={session} sessionId={session}
onSessionId={(sid) => void vm.setSession(id, sid)} onSessionId={(sid) => void setSessionWithInstrumentation("plain", sid)}
agentMode={agentId != null} agentMode={agentId != null}
portal={agentId != null ? portal : undefined} portal={agentId != null ? portal : undefined}
refitSignal={refitSignal} refitSignal={refitSignal}