Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions apps/desktop/src/main/lib/todo-daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -640,8 +652,52 @@ export class TodoDaemonClient extends EventEmitter {
// =========================================================================

async start(request: StartRequest): Promise<EmptyResponse> {
await this.ensureConnected();
return this.sendRequest<EmptyResponse>("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<EmptyResponse>("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<EmptyResponse> {
Expand All @@ -667,8 +723,31 @@ export class TodoDaemonClient extends EventEmitter {
}

async rehydrate(): Promise<EmptyResponse> {
await this.ensureConnected();
return this.sendRequest<EmptyResponse>("rehydrate", {});
try {
await this.ensureConnected();
const response = await this.sendRequest<EmptyResponse>("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<ListActiveResponse> {
Expand Down
83 changes: 83 additions & 0 deletions apps/desktop/src/main/todo-agent/daemon-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -21,9 +27,46 @@ export function startTodoAgentDaemonBridge(): Promise<void> {
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,
Expand All @@ -33,17 +76,57 @@ export function startTodoAgentDaemonBridge(): Promise<void> {
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;
Expand Down
77 changes: 77 additions & 0 deletions apps/desktop/src/main/todo-agent/debug.ts
Original file line number Diff line number Diff line change
@@ -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<TodoStreamEvent, "id" | "iteration" | "kind" | "label" | "text">,
) {
return {
eventId: event.id,
iteration: event.iteration,
kind: event.kind,
label: event.label,
textPreview: event.text,
};
}

export function getTodoStreamBatchDebugData(
sessionId: string,
events: readonly Pick<TodoStreamEvent, "kind">[],
) {
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,
};
}
Loading
Loading