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);
}
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 {
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);
sessionRef.current = currentSession;
const mountedRef = useRef(false);
const openOrAttachCountRef = useRef(0);
const onSessionIdRef = useRef(onSessionId);
onSessionIdRef.current = onSessionId;
const onConversationIdRef = useRef(onConversationId);
@ -156,7 +170,28 @@ export function CustomAgentChatView({
const reattachStructuredSession = useCallback(
async (sid: string, options: { applyScrollback?: boolean } = {}) => {
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;
setCurrentSession(reattached.sessionId);
if (options.applyScrollback) {
@ -170,14 +205,47 @@ export function CustomAgentChatView({
const recoverStructuredSession = useCallback(
async (options: { applyScrollback?: boolean; retryAttachNotFound?: boolean } = {}) => {
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);
for (;;) {
const launched = await agent.launchAgentChat(projectId, agentId, {
const launchRequest = {
cwd,
rows: 24,
cols: 80,
conversationId: conversationId ?? undefined,
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;
setCurrentSession(launched.sessionId);
@ -186,9 +254,15 @@ export function CustomAgentChatView({
onConversationIdRef.current(launched.assignedConversationId);
}
try {
return await reattachStructuredSession(launched.sessionId, {
const recoveredSessionId = await reattachStructuredSession(launched.sessionId, {
applyScrollback: options.applyScrollback,
});
console.debug("[ticket149] recoverStructuredSession:success", {
timestamp: new Date().toISOString(),
launchedSessionId: launched.sessionId,
recoveredSessionId,
});
return recoveredSessionId;
} catch (e) {
if (isNotFound(e) && retryAttachNotFound) {
retryAttachNotFound = false;
@ -198,6 +272,11 @@ export function CustomAgentChatView({
}
continue;
}
console.error("[ticket149] recoverStructuredSession:reattach:error", {
timestamp: new Date().toISOString(),
launchedSessionId: launched.sessionId,
error: instrumentationError(e),
});
throw e;
}
}
@ -223,6 +302,13 @@ export function CustomAgentChatView({
let cancelled = false;
async function openOrAttach() {
const execution = openOrAttachCountRef.current + 1;
openOrAttachCountRef.current = execution;
console.debug("[ticket149] openOrAttach:start", {
timestamp: new Date().toISOString(),
execution,
sessionId,
});
setOpening(true);
setError(null);
try {
@ -230,6 +316,11 @@ export function CustomAgentChatView({
try {
await reattachStructuredSession(sessionId, { applyScrollback: true });
if (cancelled) return;
console.debug("[ticket149] openOrAttach:reattach-existing:success", {
timestamp: new Date().toISOString(),
execution,
sessionId,
});
return;
} catch (e) {
// The backend contract for `reattach_agent_chat` (NOT_FOUND) is that
@ -240,6 +331,12 @@ export function CustomAgentChatView({
setCurrentSession(null);
onSessionIdRef.current(null);
}
console.debug("[ticket149] openOrAttach:reattach-existing:not-found", {
timestamp: new Date().toISOString(),
execution,
sessionId,
error: instrumentationError(e),
});
}
}
await recoverStructuredSession({
@ -247,7 +344,18 @@ export function CustomAgentChatView({
retryAttachNotFound: true,
});
if (cancelled) return;
console.debug("[ticket149] openOrAttach:recover:success", {
timestamp: new Date().toISOString(),
execution,
receivedSessionId: sessionId,
});
} catch (e) {
console.error("[ticket149] openOrAttach:error", {
timestamp: new Date().toISOString(),
execution,
sessionId,
error: instrumentationError(e),
});
if (!cancelled) setError(describe(e));
} finally {
if (!cancelled) setOpening(false);

View File

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