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
1 change: 1 addition & 0 deletions apps/array/src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
apiHost: string;
projectId: number;
logUrl?: string;
sdkSessionId?: string;
}): Promise<{ sessionId: string; channel: string } | null> =>
ipcRenderer.invoke("agent-reconnect", params),
onAgentEvent: (
Expand Down
15 changes: 11 additions & 4 deletions apps/array/src/main/services/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface SessionConfig {
repoPath: string;
credentials: PostHogCredentials;
logUrl?: string; // For reconnection from S3
sdkSessionId?: string; // SDK session ID for resuming Claude Code context
}

export interface ManagedSession {
Expand Down Expand Up @@ -171,7 +172,8 @@ export class SessionManager {
config: SessionConfig,
isReconnect: boolean,
): Promise<ManagedSession | null> {
const { taskId, taskRunId, repoPath, credentials, logUrl } = config;
const { taskId, taskRunId, repoPath, credentials, logUrl, sdkSessionId } =
config;

const existing = this.sessions.get(taskRunId);
if (existing) {
Expand Down Expand Up @@ -214,9 +216,12 @@ export class SessionManager {
sessionId: taskRunId,
cwd: repoPath,
mcpServers: [],
_meta: logUrl
? { persistence: { taskId, runId: taskRunId, logUrl } }
: undefined,
_meta: {
...(logUrl && {
persistence: { taskId, runId: taskRunId, logUrl },
}),
...(sdkSessionId && { sdkSessionId }),
},
});
} else {
await connection.newSession({
Expand Down Expand Up @@ -439,6 +444,7 @@ interface AgentSessionParams {
apiHost: string;
projectId: number;
logUrl?: string;
sdkSessionId?: string;
}

type SessionResponse = { sessionId: string; channel: string };
Expand Down Expand Up @@ -467,6 +473,7 @@ function toSessionConfig(params: AgentSessionParams): SessionConfig {
projectId: params.projectId,
},
logUrl: params.logUrl,
sdkSessionId: params.sdkSessionId,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
latestRunId,
logUrl: latestRunLogUrl,
});
const { notifications, rawEntries } =
const { notifications, rawEntries, sdkSessionId } =
await fetchSessionLogs(latestRunLogUrl);
log.info("Loaded historical logs", {
notifications: notifications.length,
rawEntries: rawEntries.length,
sdkSessionId,
});

// 2. Convert to SessionEvent format - interleave raw and parsed by order
Expand Down Expand Up @@ -189,6 +190,7 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
apiHost,
projectId,
logUrl: latestRunLogUrl,
sdkSessionId,
});

if (result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface StoredLogEntry {
export interface ParsedSessionLogs {
notifications: SessionNotification[];
rawEntries: StoredLogEntry[];
sdkSessionId?: string;
}

/**
Expand All @@ -36,6 +37,7 @@ export async function fetchSessionLogs(

const notifications: SessionNotification[] = [];
const rawEntries: StoredLogEntry[] = [];
let sdkSessionId: string | undefined;

for (const line of content.trim().split("\n")) {
try {
Expand Down Expand Up @@ -71,12 +73,26 @@ export async function fetchSessionLogs(
) {
notifications.push(stored.notification.params as SessionNotification);
}

// Extract SDK session ID from _posthog/sdk_session notification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to do this, isn't session id = task run id?

Copy link
Contributor Author

@jonathanlab jonathanlab Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have 4 IDs:
Task ID
Task run ID
ACP Session ID
Agent (Claude/Codex/Gemini) SDK ID (This stores context remotely, and is being retrieved in this PR)

We retrieve the logs using Task ID + Task Run ID.

The ACP Session ID is always equivalent to the task run ID.

The Agent SDKs return their own ID once a conversation is created. We store that via _posthog/sdk_session in the log, and retrieve it on session resumption. This ID is used by the provider to store context etc.See also: https://platform.claude.com/docs/en/agent-sdk/sessions

if (
stored.type === "notification" &&
stored.notification?.method?.endsWith("posthog/sdk_session") &&
stored.notification?.params
) {
const params = stored.notification.params as {
sdkSessionId?: string;
};
if (params.sdkSessionId) {
sdkSessionId = params.sdkSessionId;
}
}
} catch {
// Skip malformed lines
}
}

return { notifications, rawEntries };
return { notifications, rawEntries, sdkSessionId };
} catch {
// Network error or other failure
return { notifications: [], rawEntries: [] };
Expand Down
1 change: 1 addition & 0 deletions apps/array/src/renderer/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ declare global {
apiHost: string;
projectId: number;
logUrl?: string;
sdkSessionId?: string;
}) => Promise<{ sessionId: string; channel: string } | null>;
onAgentEvent: (
channel: string,
Expand Down
8 changes: 7 additions & 1 deletion packages/agent/src/adapters/claude/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class ClaudeAcpAgent implements Agent {
fileContentCache: { [key: string]: string };
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
clientCapabilities?: ClientCapabilities;
logger: Logger = new Logger({ debug: false, prefix: "[ClaudeAcpAgent]" });
logger: Logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
sessionStore?: SessionStore;

constructor(client: AgentSideConnection, sessionStore?: SessionStore) {
Expand Down Expand Up @@ -934,6 +934,7 @@ export class ClaudeAcpAgent implements Agent {
async resumeSession(
params: LoadSessionRequest,
): Promise<LoadSessionResponse> {
this.logger.info("[RESUME] Resuming session", { params });
const { sessionId } = params;

// Extract persistence config and SDK session ID from _meta
Expand Down Expand Up @@ -980,6 +981,11 @@ export class ClaudeAcpAgent implements Agent {

const permissionMode = "default";

this.logger.info("Resuming session", {
cwd: params.cwd,
sdkSessionId,
persistence,
});
const options: Options = {
cwd: params.cwd,
includePartialMessages: true,
Expand Down