diff --git a/apps/desktop/src/main/lib/todo-daemon/client.ts b/apps/desktop/src/main/lib/todo-daemon/client.ts index 02921275bae..756eecfc62e 100644 --- a/apps/desktop/src/main/lib/todo-daemon/client.ts +++ b/apps/desktop/src/main/lib/todo-daemon/client.ts @@ -27,6 +27,7 @@ import { connect, type Socket } from "node:net"; import { homedir } from "node:os"; import { join } from "node:path"; import { app } from "electron"; +import { todoAgentMainDebug } from "main/todo-agent/debug"; import { SUPERSET_DIR_NAME } from "shared/constants"; import { type AbortRequest, @@ -221,6 +222,17 @@ export class TodoDaemonClient extends EventEmitter { this.activeSessionIds = Array.isArray(response.activeSessionIds) ? response.activeSessionIds.slice() : []; + todoAgentMainDebug.info( + "todo-daemon-client-authenticated", + { + protocolVersion: response.protocolVersion, + activeSessionCount: this.activeSessionIds.length, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-client-authenticated"], + }, + ); return; } catch (error) { if (attempt === 0) { @@ -640,8 +652,52 @@ export class TodoDaemonClient extends EventEmitter { // ========================================================================= async start(request: StartRequest): Promise { - await this.ensureConnected(); - return this.sendRequest("start", request); + todoAgentMainDebug.info( + "todo-daemon-client-start-request", + { + sessionId: request.sessionId, + fromScheduledWakeup: request.fromScheduledWakeup ?? false, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-client-start-request"], + }, + ); + try { + await this.ensureConnected(); + const response = await this.sendRequest("start", request); + todoAgentMainDebug.info( + "todo-daemon-client-start-request-success", + { + sessionId: request.sessionId, + fromScheduledWakeup: request.fromScheduledWakeup ?? false, + }, + { + captureMessage: true, + fingerprint: [ + "todo.agent.main", + "todo-daemon-client-start-request-success", + ], + }, + ); + return response; + } catch (error) { + todoAgentMainDebug.captureException( + error, + "todo-daemon-client-start-request-failed", + { + sessionId: request.sessionId, + fromScheduledWakeup: request.fromScheduledWakeup ?? false, + }, + { + fingerprint: [ + "todo.agent.main", + "todo-daemon-client-start-request-failed", + ], + }, + ); + throw error; + } } async abort(request: AbortRequest): Promise { @@ -667,8 +723,31 @@ export class TodoDaemonClient extends EventEmitter { } async rehydrate(): Promise { - await this.ensureConnected(); - return this.sendRequest("rehydrate", {}); + try { + await this.ensureConnected(); + const response = await this.sendRequest("rehydrate", {}); + todoAgentMainDebug.info( + "todo-daemon-client-rehydrate-success", + { + activeSessionCount: this.activeSessionIds.length, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-client-rehydrate-success"], + }, + ); + return response; + } catch (error) { + todoAgentMainDebug.captureException( + error, + "todo-daemon-client-rehydrate-failed", + undefined, + { + fingerprint: ["todo.agent.main", "todo-daemon-client-rehydrate-failed"], + }, + ); + throw error; + } } async listActive(): Promise { diff --git a/apps/desktop/src/main/todo-agent/daemon-bridge.ts b/apps/desktop/src/main/todo-agent/daemon-bridge.ts index 413631f600f..c4b976e6b86 100644 --- a/apps/desktop/src/main/todo-agent/daemon-bridge.ts +++ b/apps/desktop/src/main/todo-agent/daemon-bridge.ts @@ -2,6 +2,12 @@ import { disposeTodoDaemonClient, getTodoDaemonClient, } from "main/lib/todo-daemon/client"; +import { + getTodoSessionDebugData, + getTodoStreamBatchDebugData, + getTodoStreamEventDebugData, + todoAgentMainDebug, +} from "./debug"; import { getTodoSessionStore } from "./session-store"; /** @@ -21,9 +27,46 @@ export function startTodoAgentDaemonBridge(): Promise { if (!wired) { wired = true; client.on("sessionState", (payload) => { + todoAgentMainDebug.info( + "todo-daemon-bridge-session-state", + getTodoSessionDebugData(payload.session), + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-bridge-session-state"], + }, + ); getTodoSessionStore().externalEmit(payload.session); }); client.on("streamEvents", (payload) => { + todoAgentMainDebug.info( + "todo-daemon-bridge-stream-batch", + getTodoStreamBatchDebugData(payload.sessionId, payload.events), + ); + for (const event of payload.events) { + if ( + event.kind !== "system_init" && + event.kind !== "error" && + event.kind !== "remote_control" && + event.kind !== "remote_control_error" + ) { + continue; + } + todoAgentMainDebug.info( + "todo-daemon-bridge-stream-event", + { + sessionId: payload.sessionId, + ...getTodoStreamEventDebugData(event), + }, + { + captureMessage: true, + fingerprint: [ + "todo.agent.main", + "todo-daemon-bridge-stream-event", + event.kind, + ], + }, + ); + } getTodoSessionStore().externalEmitStream( payload.sessionId, payload.events, @@ -33,17 +76,57 @@ export function startTodoAgentDaemonBridge(): Promise { console.warn( "[todo-agent] daemon disconnected — will reconnect on next RPC", ); + todoAgentMainDebug.warn( + "todo-daemon-bridge-disconnected", + undefined, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-bridge-disconnected"], + }, + ); }); client.on("error", (error) => { console.warn("[todo-agent] daemon client error", error); + todoAgentMainDebug.captureException( + error, + "todo-daemon-bridge-error", + undefined, + { + fingerprint: ["todo.agent.main", "todo-daemon-bridge-error"], + }, + ); }); } connectPromise = (async () => { + todoAgentMainDebug.info( + "todo-daemon-bridge-init", + undefined, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-bridge-init"], + }, + ); try { await client.ensureConnected(); await client.rehydrate(); + todoAgentMainDebug.info( + "todo-daemon-bridge-init-success", + undefined, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-daemon-bridge-init-success"], + }, + ); } catch (error) { console.warn("[todo-agent] daemon bridge failed to initialize", error); + todoAgentMainDebug.captureException( + error, + "todo-daemon-bridge-init-failed", + undefined, + { + fingerprint: ["todo.agent.main", "todo-daemon-bridge-init-failed"], + }, + ); // Drop the cached promise so a later retry can try again. connectPromise = null; throw error; diff --git a/apps/desktop/src/main/todo-agent/debug.ts b/apps/desktop/src/main/todo-agent/debug.ts new file mode 100644 index 00000000000..51e2f3f7e9b --- /dev/null +++ b/apps/desktop/src/main/todo-agent/debug.ts @@ -0,0 +1,77 @@ +import type { SelectTodoSession } from "@superset/local-db"; +import type { TodoStreamEvent } from "./types"; +import { createMainDebugChannel } from "../lib/debug-channel"; + +const DEBUG_TODO_AGENT = process.env.SUPERSET_TODO_DEBUG === "1"; + +// TODO Agent の作成から daemon 実行、PTY / Remote Control 分岐までを +// 一回の Sentry ログで追えるようにするための main/daemon 共通 logger。 +// 主に見たいのは: +// - renderer から送った PTY / Remote の意図が main で落ちていないか +// - runtime-config.json に正しく保存 / 読み出しできたか +// - daemon が headless / PTY / Remote Control をどう最終判定したか +// - PTY 起動後に Remote Control URL 発行まで進んだか +// Sentry には常時送り、console ミラーだけ env フラグで制御する。 +export const todoAgentMainDebug = createMainDebugChannel({ + namespace: "todo.agent.main", + enabled: true, + mirrorToConsole: DEBUG_TODO_AGENT, +}); + +export function getTodoSessionDebugData( + session: Pick< + SelectTodoSession, + | "id" + | "workspaceId" + | "projectId" + | "status" + | "phase" + | "iteration" + | "artifactPath" + | "remoteControlEnabled" + | "claudeSessionId" + | "verdictReason" + | "waitingReason" + > +) { + return { + sessionId: session.id, + workspaceId: session.workspaceId, + projectId: session.projectId ?? null, + status: session.status, + phase: session.phase, + iteration: session.iteration, + artifactPath: session.artifactPath, + remoteControlEnabled: session.remoteControlEnabled ?? false, + hasClaudeSessionId: Boolean(session.claudeSessionId), + verdictReason: session.verdictReason ?? null, + waitingReason: session.waitingReason ?? null, + }; +} + +export function getTodoStreamEventDebugData( + event: Pick, +) { + return { + eventId: event.id, + iteration: event.iteration, + kind: event.kind, + label: event.label, + textPreview: event.text, + }; +} + +export function getTodoStreamBatchDebugData( + sessionId: string, + events: readonly Pick[], +) { + const kinds = Array.from(new Set(events.map((event) => event.kind))); + const lastEvent = events.length > 0 ? events[events.length - 1] : null; + return { + sessionId, + eventCount: events.length, + eventKinds: kinds.join(","), + firstKind: events[0]?.kind ?? null, + lastKind: lastEvent?.kind ?? null, + }; +} diff --git a/apps/desktop/src/main/todo-agent/runtime-config.ts b/apps/desktop/src/main/todo-agent/runtime-config.ts index f2052c29818..31033494a1a 100644 --- a/apps/desktop/src/main/todo-agent/runtime-config.ts +++ b/apps/desktop/src/main/todo-agent/runtime-config.ts @@ -1,5 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; +import { todoAgentMainDebug } from "./debug"; const TODO_RUNTIME_CONFIG_FILE = "runtime-config.json"; @@ -33,11 +34,38 @@ export function readTodoSessionRuntimeConfig(params: { remoteControlEnabled: legacyRemoteControlEnabled, }; if (!path.isAbsolute(params.artifactPath)) { + todoAgentMainDebug.warn( + "todo-runtime-config-read-fallback", + { + artifactPath: params.artifactPath, + reason: "artifact-path-not-absolute", + fallbackPtyEnabled: legacyFallback.ptyEnabled, + fallbackRemoteControlEnabled: legacyFallback.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-read-fallback"], + }, + ); return legacyFallback; } const filePath = getRuntimeConfigPath(params.artifactPath); if (!existsSync(filePath)) { + todoAgentMainDebug.warn( + "todo-runtime-config-read-fallback", + { + artifactPath: params.artifactPath, + filePath, + reason: "runtime-config-missing", + fallbackPtyEnabled: legacyFallback.ptyEnabled, + fallbackRemoteControlEnabled: legacyFallback.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-read-fallback"], + }, + ); return legacyFallback; } @@ -46,14 +74,56 @@ export function readTodoSessionRuntimeConfig(params: { readFileSync(filePath, "utf8"), ) as Partial | null; if (!parsed || typeof parsed !== "object") { + todoAgentMainDebug.warn( + "todo-runtime-config-read-fallback", + { + artifactPath: params.artifactPath, + filePath, + reason: "runtime-config-invalid-json-shape", + fallbackPtyEnabled: legacyFallback.ptyEnabled, + fallbackRemoteControlEnabled: legacyFallback.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-read-fallback"], + }, + ); return legacyFallback; } - return normalizeConfig({ + const normalized = normalizeConfig({ ptyEnabled: parsed.ptyEnabled === true, remoteControlEnabled: parsed.remoteControlEnabled === true, }); + todoAgentMainDebug.info( + "todo-runtime-config-read", + { + artifactPath: params.artifactPath, + filePath, + ptyEnabled: normalized.ptyEnabled, + remoteControlEnabled: normalized.remoteControlEnabled, + usedFallback: false, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-read"], + }, + ); + return normalized; } catch (error) { console.warn("[todo-agent] failed to read runtime config", error); + todoAgentMainDebug.captureException( + error, + "todo-runtime-config-read-failed", + { + artifactPath: params.artifactPath, + filePath, + fallbackPtyEnabled: legacyFallback.ptyEnabled, + fallbackRemoteControlEnabled: legacyFallback.remoteControlEnabled, + }, + { + fingerprint: ["todo.agent.main", "todo-runtime-config-read-failed"], + }, + ); return legacyFallback; } } @@ -62,15 +132,57 @@ export function writeTodoSessionRuntimeConfig( artifactPath: string, config: TodoSessionRuntimeConfig, ): void { - if (!path.isAbsolute(artifactPath)) return; + if (!path.isAbsolute(artifactPath)) { + todoAgentMainDebug.warn( + "todo-runtime-config-write-skipped", + { + artifactPath, + reason: "artifact-path-not-absolute", + ptyEnabled: config.ptyEnabled, + remoteControlEnabled: config.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-write-skipped"], + }, + ); + return; + } try { + const normalized = normalizeConfig(config); + const filePath = getRuntimeConfigPath(artifactPath); mkdirSync(artifactPath, { recursive: true }); writeFileSync( - getRuntimeConfigPath(artifactPath), - `${JSON.stringify(normalizeConfig(config), null, 2)}\n`, + filePath, + `${JSON.stringify(normalized, null, 2)}\n`, "utf8", ); + todoAgentMainDebug.info( + "todo-runtime-config-write", + { + artifactPath, + filePath, + ptyEnabled: normalized.ptyEnabled, + remoteControlEnabled: normalized.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-runtime-config-write"], + }, + ); } catch (error) { console.warn("[todo-agent] failed to write runtime config", error); + todoAgentMainDebug.captureException( + error, + "todo-runtime-config-write-failed", + { + artifactPath, + ptyEnabled: config.ptyEnabled, + remoteControlEnabled: config.remoteControlEnabled, + }, + { + fingerprint: ["todo.agent.main", "todo-runtime-config-write-failed"], + }, + ); } } diff --git a/apps/desktop/src/main/todo-agent/supervisor.ts b/apps/desktop/src/main/todo-agent/supervisor.ts index f0b36559e2d..5ea996798c3 100644 --- a/apps/desktop/src/main/todo-agent/supervisor.ts +++ b/apps/desktop/src/main/todo-agent/supervisor.ts @@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import type { SelectTodoSession } from "@superset/local-db"; import { getTodoDaemonClient } from "main/lib/todo-daemon/client"; +import { getTodoSessionDebugData, todoAgentMainDebug } from "./debug"; import { getTodoSessionStore, resolveWorktreePath } from "./session-store"; import { TODO_ARTIFACT_SUBDIR } from "./types"; @@ -41,11 +42,35 @@ class TodoSupervisor { sessionId: string, options?: { fromScheduledWakeup?: boolean }, ): Promise { + const current = getTodoSessionStore().get(sessionId); + todoAgentMainDebug.info( + "todo-supervisor-start", + { + sessionId, + fromScheduledWakeup: options?.fromScheduledWakeup ?? false, + ...(current ? getTodoSessionDebugData(current) : {}), + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-supervisor-start"], + }, + ); try { await getTodoDaemonClient().start({ sessionId, fromScheduledWakeup: options?.fromScheduledWakeup, }); + todoAgentMainDebug.info( + "todo-supervisor-start-success", + { + sessionId, + fromScheduledWakeup: options?.fromScheduledWakeup ?? false, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-supervisor-start-success"], + }, + ); } catch (error) { // The tRPC router flips the session to `preparing` before // fire-and-forgetting us, so a daemon spawn/connect/auth @@ -55,6 +80,18 @@ class TodoSupervisor { // can retry or delete the session. const reason = error instanceof Error ? error.message : String(error); console.warn("[todo-supervisor] daemon start failed", error); + todoAgentMainDebug.captureException( + error, + "todo-supervisor-start-failed", + { + sessionId, + fromScheduledWakeup: options?.fromScheduledWakeup ?? false, + errorMessage: reason, + }, + { + fingerprint: ["todo.agent.main", "todo-supervisor-start-failed"], + }, + ); try { const store = getTodoSessionStore(); const current = store.get(sessionId); diff --git a/apps/desktop/src/main/todo-agent/trpc-router.ts b/apps/desktop/src/main/todo-agent/trpc-router.ts index 5d3a2f439df..7d47d78ff2f 100644 --- a/apps/desktop/src/main/todo-agent/trpc-router.ts +++ b/apps/desktop/src/main/todo-agent/trpc-router.ts @@ -25,6 +25,7 @@ import { computeNextRunAt, getTodoScheduler } from "./scheduler"; import { getTodoSessionStore, resolveWorktreePath } from "./session-store"; import { getTodoSettings, updateTodoSettings } from "./settings"; import { getTodoSupervisor } from "./supervisor"; +import { getTodoSessionDebugData, todoAgentMainDebug } from "./debug"; import { TODO_ARTIFACT_SUBDIR, type TodoScheduleFireEvent, @@ -52,104 +53,153 @@ export const createTodoAgentRouter = () => { create: publicProcedure .input(todoCreateInputSchema) .mutation(async ({ input }) => { - // When the UI creates a fresh workspace+worktree immediately - // before creating the TODO (the "新しい worktree を作成して実行" - // checkbox), `workspaces.create` returns while `git worktree - // add` is still running in the background. Materializing the - // artifact directory now would mkdir inside the future - // worktree path, leaving it non-empty and causing the - // subsequent `git worktree add` to fail — the symptom users - // see as the sidebar error + "ブランチ取得中…" that never - // resolves. Block until init is done (or already no-op) so - // prepareArtifacts runs against a real worktree. - // - // `waitForInit` has a 30s internal timeout that resolves - // silently even if init is still running, so a slow - // fetch/clone path could still slip through. Loop on the - // `isInitializing` flag so we really block until the job - // reaches a terminal state, up to a generous ceiling. - const INIT_WAIT_STEP_MS = 30_000; - const INIT_WAIT_CEILING_MS = 10 * 60_000; - const waitStartedAt = Date.now(); - while (workspaceInitManager.isInitializing(input.workspaceId)) { - if (Date.now() - waitStartedAt > INIT_WAIT_CEILING_MS) { + todoAgentMainDebug.info( + "todo-create-request", + { + workspaceId: input.workspaceId, + projectId: input.projectId ?? null, + ptyEnabled: input.ptyEnabled, + remoteControlEnabled: input.remoteControlEnabled, + maxIterations: input.maxIterations, + maxWallClockSec: input.maxWallClockSec, + hasVerify: (input.verifyCommand?.trim().length ?? 0) > 0, + hasCustomSystemPrompt: + (input.customSystemPrompt?.trim().length ?? 0) > 0, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-create-request"], + }, + ); + try { + // When the UI creates a fresh workspace+worktree immediately + // before creating the TODO (the "新しい worktree を作成して実行" + // checkbox), `workspaces.create` returns while `git worktree + // add` is still running in the background. Materializing the + // artifact directory now would mkdir inside the future + // worktree path, leaving it non-empty and causing the + // subsequent `git worktree add` to fail — the symptom users + // see as the sidebar error + "ブランチ取得中…" that never + // resolves. Block until init is done (or already no-op) so + // prepareArtifacts runs against a real worktree. + // + // `waitForInit` has a 30s internal timeout that resolves + // silently even if init is still running, so a slow + // fetch/clone path could still slip through. Loop on the + // `isInitializing` flag so we really block until the job + // reaches a terminal state, up to a generous ceiling. + const INIT_WAIT_STEP_MS = 30_000; + const INIT_WAIT_CEILING_MS = 10 * 60_000; + const waitStartedAt = Date.now(); + while (workspaceInitManager.isInitializing(input.workspaceId)) { + if (Date.now() - waitStartedAt > INIT_WAIT_CEILING_MS) { + throw new TRPCError({ + code: "TIMEOUT", + message: `todo-agent: workspace ${input.workspaceId} の初期化が時間内に終わりませんでした`, + }); + } + await workspaceInitManager.waitForInit( + input.workspaceId, + INIT_WAIT_STEP_MS, + ); + } + if (workspaceInitManager.hasFailed(input.workspaceId)) { throw new TRPCError({ - code: "TIMEOUT", - message: `todo-agent: workspace ${input.workspaceId} の初期化が時間内に終わりませんでした`, + code: "PRECONDITION_FAILED", + message: `todo-agent: workspace ${input.workspaceId} の初期化に失敗しました`, }); } - await workspaceInitManager.waitForInit( - input.workspaceId, - INIT_WAIT_STEP_MS, - ); - } - if (workspaceInitManager.hasFailed(input.workspaceId)) { - throw new TRPCError({ - code: "PRECONDITION_FAILED", - message: `todo-agent: workspace ${input.workspaceId} の初期化に失敗しました`, - }); - } - const store = getTodoSessionStore(); - const worktreePath = resolveWorktreePath(input.workspaceId); - if (!worktreePath) { - throw new Error( - `todo-agent: workspace ${input.workspaceId} のパスを解決できませんでした`, - ); - } + const store = getTodoSessionStore(); + const worktreePath = resolveWorktreePath(input.workspaceId); + if (!worktreePath) { + throw new Error( + `todo-agent: workspace ${input.workspaceId} のパスを解決できませんでした`, + ); + } - // Compute the final artifact path up-front so the row is - // inserted with its permanent path in one shot. No more - // half-written PENDING rows left behind if the process - // crashes between insert and update. - const sessionId = randomUUID(); - const supervisor = getTodoSupervisor(); - const artifactPath = supervisor.computeArtifactPath({ - sessionId, - workspaceId: input.workspaceId, - }); + // Compute the final artifact path up-front so the row is + // inserted with its permanent path in one shot. No more + // half-written PENDING rows left behind if the process + // crashes between insert and update. + const sessionId = randomUUID(); + const supervisor = getTodoSupervisor(); + const artifactPath = supervisor.computeArtifactPath({ + sessionId, + workspaceId: input.workspaceId, + }); - // Fall through to the user's configured defaults when the - // composer did not pick an explicit model / effort. Null - // at both levels means "use Claude Code's own default" - // (we simply omit the CLI flag at spawn time). - const settings = getTodoSettings(); - const resolvedModel = - input.claudeModel !== undefined - ? input.claudeModel - : (settings.defaultClaudeModel ?? null); - const resolvedEffort = - input.claudeEffort !== undefined - ? input.claudeEffort - : (settings.defaultClaudeEffort ?? null); - - const session = store.insertQueuedFromTemplate({ - id: sessionId, - projectId: input.projectId ?? null, - workspaceId: input.workspaceId, - title: input.title ?? "", - description: input.description, - goal: input.goal, - verifyCommand: input.verifyCommand, - maxIterations: input.maxIterations, - maxWallClockSec: input.maxWallClockSec, - customSystemPrompt: input.customSystemPrompt, - claudeModel: resolvedModel, - claudeEffort: resolvedEffort, - remoteControlEnabled: input.remoteControlEnabled, - artifactPath, - }); + // Fall through to the user's configured defaults when the + // composer did not pick an explicit model / effort. Null + // at both levels means "use Claude Code's own default" + // (we simply omit the CLI flag at spawn time). + const settings = getTodoSettings(); + const resolvedModel = + input.claudeModel !== undefined + ? input.claudeModel + : (settings.defaultClaudeModel ?? null); + const resolvedEffort = + input.claudeEffort !== undefined + ? input.claudeEffort + : (settings.defaultClaudeEffort ?? null); + + const session = store.insertQueuedFromTemplate({ + id: sessionId, + projectId: input.projectId ?? null, + workspaceId: input.workspaceId, + title: input.title ?? "", + description: input.description, + goal: input.goal, + verifyCommand: input.verifyCommand, + maxIterations: input.maxIterations, + maxWallClockSec: input.maxWallClockSec, + customSystemPrompt: input.customSystemPrompt, + claudeModel: resolvedModel, + claudeEffort: resolvedEffort, + remoteControlEnabled: input.remoteControlEnabled, + artifactPath, + }); - // Materialize the directory + goal.md. If this throws after - // the row exists the user can delete the broken session - // from the Manager — same as any other filesystem error. - supervisor.prepareArtifacts(session); - writeTodoSessionRuntimeConfig(session.artifactPath, { - ptyEnabled: input.ptyEnabled, - remoteControlEnabled: input.remoteControlEnabled, - }); + // Materialize the directory + goal.md. If this throws after + // the row exists the user can delete the broken session + // from the Manager — same as any other filesystem error. + supervisor.prepareArtifacts(session); + writeTodoSessionRuntimeConfig(session.artifactPath, { + ptyEnabled: input.ptyEnabled, + remoteControlEnabled: input.remoteControlEnabled, + }); - return { sessionId: session.id }; + todoAgentMainDebug.info( + "todo-create-request-success", + { + ...getTodoSessionDebugData(session), + ptyEnabled: input.ptyEnabled, + remoteControlEnabled: input.remoteControlEnabled, + claudeModel: resolvedModel, + claudeEffort: resolvedEffort, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-create-request-success"], + }, + ); + return { sessionId: session.id }; + } catch (error) { + todoAgentMainDebug.captureException( + error, + "todo-create-request-failed", + { + workspaceId: input.workspaceId, + projectId: input.projectId ?? null, + ptyEnabled: input.ptyEnabled, + remoteControlEnabled: input.remoteControlEnabled, + }, + { + fingerprint: ["todo.agent.main", "todo-create-request-failed"], + }, + ); + throw error; + } }), list: publicProcedure @@ -226,6 +276,23 @@ export const createTodoAgentRouter = () => { waitingUntil: null, waitingReason: null, }); + const runtimeConfig = readTodoSessionRuntimeConfig({ + artifactPath: session.artifactPath, + fallbackRemoteControlEnabled: session.remoteControlEnabled ?? false, + }); + todoAgentMainDebug.info( + "todo-start-request", + { + ...getTodoSessionDebugData(session), + runtimeConfigPtyEnabled: runtimeConfig.ptyEnabled, + runtimeConfigRemoteControlEnabled: + runtimeConfig.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-start-request"], + }, + ); // Fire-and-forget: the supervisor drives the rest of the loop. void getTodoSupervisor().start(input.sessionId); return { ok: true }; @@ -412,6 +479,14 @@ export const createTodoAgentRouter = () => { message: "元セッションが見つかりません", }); } + todoAgentMainDebug.info( + "todo-rerun-request", + getTodoSessionDebugData(source), + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-rerun-request"], + }, + ); // Create a brand-new queued session that copies the user- // authored fields from the source. Verdict / iteration / @@ -465,6 +540,20 @@ export const createTodoAgentRouter = () => { fallbackRemoteControlEnabled: source.remoteControlEnabled ?? false, }); writeTodoSessionRuntimeConfig(next.artifactPath, runtimeConfig); + todoAgentMainDebug.info( + "todo-rerun-request-success", + { + sourceSessionId: source.id, + nextSessionId: next.id, + nextArtifactPath: next.artifactPath, + ptyEnabled: runtimeConfig.ptyEnabled, + remoteControlEnabled: runtimeConfig.remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.main", "todo-rerun-request-success"], + }, + ); return { sessionId: next.id }; }), diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx index a840a7892f3..984ca5e2366 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx @@ -71,6 +71,7 @@ import { LuPanelRightOpen, } from "react-icons/lu"; import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; +import { todoAgentRendererDebug } from "renderer/features/todo-agent/debug"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { type ClaudeEffortPick, @@ -2800,6 +2801,29 @@ function TodoComposer({ goal.trim(), goalAttachments, ); + todoAgentRendererDebug.info( + "todo-create-submit", + { + source: "todo-manager-composer", + workspaceId: targetWorkspaceId, + projectId, + createWorktree, + ptyEnabled, + remoteControlEnabled, + titleLength: title.trim().length, + descriptionLength: resolvedDescription.length, + hasGoal: resolvedGoal.length > 0, + hasVerify: verifyCommand.trim().length > 0, + }, + { + captureMessage: true, + fingerprint: [ + "todo.agent.renderer", + "todo-create-submit", + "todo-manager-composer", + ], + }, + ); const res = await createMut.mutateAsync({ workspaceId: targetWorkspaceId, projectId, @@ -2815,6 +2839,24 @@ function TodoComposer({ ptyEnabled, remoteControlEnabled, }); + todoAgentRendererDebug.info( + "todo-create-submit-success", + { + source: "todo-manager-composer", + sessionId: res.sessionId, + workspaceId: targetWorkspaceId, + ptyEnabled, + remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: [ + "todo.agent.renderer", + "todo-create-submit-success", + "todo-manager-composer", + ], + }, + ); await utils.todoAgent.listAll.invalidate(); toast.success( createWorktree @@ -2826,6 +2868,27 @@ function TodoComposer({ // to unmount via onCreated → re-enabling the button would // flicker. The finally below only runs on error / throw. } catch (error) { + todoAgentRendererDebug.captureException( + error, + "todo-create-submit-failed", + { + source: "todo-manager-composer", + projectId, + workspaceId, + createWorktree, + ptyEnabled, + remoteControlEnabled, + errorMessage: + error instanceof Error ? error.message : "作成に失敗しました", + }, + { + fingerprint: [ + "todo.agent.renderer", + "todo-create-submit-failed", + "todo-manager-composer", + ], + }, + ); toast.error( error instanceof Error ? error.message : "作成に失敗しました", ); diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx index d89cb0b9a26..587ad5e0049 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx @@ -33,6 +33,7 @@ import { toPersistedEffort, toPersistedModel, } from "../ClaudeRuntimePicker"; +import { todoAgentRendererDebug } from "../debug"; import { EnhanceButton } from "./components/EnhanceButton"; interface TodoModalProps { @@ -199,6 +200,25 @@ export function TodoModal({ targetWorkspaceId = result.workspace.id; } + todoAgentRendererDebug.info( + "todo-create-submit", + { + source: "todo-modal", + workspaceId: targetWorkspaceId, + projectId: projectId ?? null, + createWorktree, + ptyEnabled, + remoteControlEnabled, + titleLength: title.trim().length, + descriptionLength: description.trim().length, + hasGoal, + hasVerify, + }, + { + captureMessage: true, + fingerprint: ["todo.agent.renderer", "todo-create-submit", "todo-modal"], + }, + ); const created = await create.mutateAsync({ workspaceId: targetWorkspaceId, projectId, @@ -214,6 +234,24 @@ export function TodoModal({ ptyEnabled, remoteControlEnabled, }); + todoAgentRendererDebug.info( + "todo-create-submit-success", + { + source: "todo-modal", + sessionId: created.sessionId, + workspaceId: targetWorkspaceId, + ptyEnabled, + remoteControlEnabled, + }, + { + captureMessage: true, + fingerprint: [ + "todo.agent.renderer", + "todo-create-submit-success", + "todo-modal", + ], + }, + ); if (createWorktree) { toast.success( "新しい worktree を作成して TODO セッションを紐付けました", @@ -226,6 +264,22 @@ export function TodoModal({ } catch (error) { const message = error instanceof Error ? error.message : "作成に失敗しました"; + todoAgentRendererDebug.captureException( + error, + "todo-create-submit-failed", + { + source: "todo-modal", + workspaceId, + projectId: projectId ?? null, + createWorktree, + ptyEnabled, + remoteControlEnabled, + errorMessage: message, + }, + { + fingerprint: ["todo.agent.renderer", "todo-create-submit-failed", "todo-modal"], + }, + ); toast.error(message); setSubmitting(false); } diff --git a/apps/desktop/src/renderer/features/todo-agent/debug.ts b/apps/desktop/src/renderer/features/todo-agent/debug.ts new file mode 100644 index 00000000000..80d2fc7e45b --- /dev/null +++ b/apps/desktop/src/renderer/features/todo-agent/debug.ts @@ -0,0 +1,20 @@ +import { createRendererDebugChannel } from "renderer/lib/debug-channel"; + +function isTodoAgentDebugEnabled(): boolean { + try { + return globalThis.localStorage?.getItem("SUPERSET_TODO_DEBUG") === "1"; + } catch { + return false; + } +} + +// TODO Agent の renderer 側 logger。 +// 作成 UI は TodoModal と AgentManager 内 composer の 2 系統あるため、 +// どちらから、どの PTY / Remote Control フラグで submit されたかを残す。 +// これにより main 側の runtime-config / daemon 判定ログと source を +// sessionId 単位で付き合わせられる。 +export const todoAgentRendererDebug = createRendererDebugChannel({ + namespace: "todo.agent.renderer", + enabled: true, + mirrorToConsole: isTodoAgentDebugEnabled(), +});