Skip to content

Commit e38a01a

Browse files
authored
Improved session ID handling (#933)
1 parent f82eb5c commit e38a01a

File tree

9 files changed

+92
-78
lines changed

9 files changed

+92
-78
lines changed

apps/twig/src/main/services/agent/service.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ interface ManagedSession {
197197
promptPending: boolean;
198198
pendingContext?: string;
199199
configOptions?: SessionConfigOption[];
200-
sessionId: string;
201200
}
202201

203202
function getClaudeCliPath(): string {
@@ -419,7 +418,6 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
419418
repoPath: rawRepoPath,
420419
credentials,
421420
logUrl,
422-
sessionId: existingSessionId,
423421
adapter,
424422
additionalDirectories,
425423
permissionMode,
@@ -534,19 +532,22 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
534532
configOptions = loadResponse.configOptions ?? undefined;
535533
agentSessionId = config.sessionId;
536534
} else if (isReconnect && adapter !== "codex") {
535+
if (!config.sessionId) {
536+
throw new Error("Cannot resume session without sessionId");
537+
}
537538
const systemPrompt = this.buildPostHogSystemPrompt(credentials);
538539
const resumeResponse = await connection.extMethod(
539540
"_posthog/session/resume",
540541
{
541-
sessionId: taskRunId,
542+
sessionId: config.sessionId,
542543
cwd: repoPath,
543544
mcpServers,
544545
_meta: {
545546
...(logUrl && {
546547
persistence: { taskId, runId: taskRunId, logUrl },
547548
}),
548549
taskRunId,
549-
...(existingSessionId && { sessionId: existingSessionId }),
550+
sessionId: config.sessionId,
550551
systemPrompt,
551552
...(permissionMode && { permissionMode }),
552553
...(additionalDirectories?.length && {
@@ -563,7 +564,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
563564
}
564565
| undefined;
565566
configOptions = resumeMeta?.configOptions;
566-
agentSessionId = (resumeResponse?.sessionId as string) ?? taskRunId;
567+
agentSessionId = config.sessionId;
567568
} else {
568569
const systemPrompt = this.buildPostHogSystemPrompt(credentials);
569570
const newSessionResponse = await connection.newSession({
@@ -600,7 +601,6 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
600601
needsRecreation: false,
601602
promptPending: false,
602603
configOptions,
603-
sessionId: agentSessionId,
604604
};
605605

606606
this.sessions.set(taskRunId, session);
@@ -706,7 +706,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
706706

707707
try {
708708
const result = await session.clientSideConnection.prompt({
709-
sessionId: session.sessionId,
709+
sessionId: session.config.sessionId!,
710710
prompt: finalPrompt,
711711
});
712712
return {
@@ -718,7 +718,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
718718
log.warn("Auth error during prompt, recreating session", { sessionId });
719719
session = await this.recreateSession(sessionId);
720720
const result = await session.clientSideConnection.prompt({
721-
sessionId: session.sessionId,
721+
sessionId: session.config.sessionId!,
722722
prompt: finalPrompt,
723723
});
724724
return {
@@ -762,7 +762,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
762762

763763
try {
764764
await session.clientSideConnection.cancel({
765-
sessionId: session.sessionId,
765+
sessionId: session.config.sessionId!,
766766
_meta: reason ? { interruptReason: reason } : undefined,
767767
});
768768
if (reason) {
@@ -792,7 +792,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
792792

793793
try {
794794
const result = await session.clientSideConnection.setSessionConfigOption({
795-
sessionId: session.sessionId,
795+
sessionId: session.config.sessionId!,
796796
configId,
797797
value,
798798
});

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ function buildConversationItems(events: AcpMessage[]): ConversationItem[] {
312312

313313
currentTurn = {
314314
type: "turn",
315-
id: `turn-${msg.id}`,
315+
id: `turn-${event.ts}-${msg.id}`,
316316
promptId: msg.id,
317317
userContent,
318318
items: [],
@@ -496,16 +496,22 @@ function processSessionUpdate(turn: Turn, update: SessionUpdate) {
496496
turn.items.push(update);
497497
break;
498498

499-
// Handle custom session updates
500499
default: {
501-
// Check for our custom session update types
502500
const customUpdate = update as unknown as {
503501
sessionUpdate: string;
502+
content?: { type: string; text?: string };
504503
status?: string;
505504
errorType?: string;
506505
message?: string;
507506
};
508-
if (
507+
if (customUpdate.sessionUpdate === "agent_message") {
508+
if (customUpdate.content?.type === "text") {
509+
appendTextChunk(turn, {
510+
sessionUpdate: "agent_message_chunk" as const,
511+
content: customUpdate.content as { type: "text"; text: string },
512+
});
513+
}
514+
} else if (
509515
customUpdate.sessionUpdate === "status" ||
510516
customUpdate.sessionUpdate === "error"
511517
) {

packages/agent/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,28 +72,28 @@
7272
},
7373
"devDependencies": {
7474
"@changesets/cli": "^2.27.8",
75+
"@posthog/shared": "workspace:*",
76+
"@twig/git": "workspace:*",
7577
"@types/bun": "latest",
7678
"@types/tar": "^6.1.13",
7779
"minimatch": "^10.0.3",
78-
"@posthog/shared": "workspace:*",
79-
"@twig/git": "workspace:*",
8080
"msw": "^2.12.7",
8181
"tsup": "^8.5.1",
8282
"tsx": "^4.20.6",
8383
"typescript": "^5.5.0",
8484
"vitest": "^2.1.8"
8585
},
8686
"dependencies": {
87+
"@agentclientprotocol/sdk": "^0.14.0",
88+
"@anthropic-ai/claude-agent-sdk": "0.2.42",
89+
"@anthropic-ai/sdk": "^0.71.0",
90+
"@hono/node-server": "^1.19.9",
91+
"@modelcontextprotocol/sdk": "^1.25.3",
8792
"@opentelemetry/api-logs": "^0.208.0",
8893
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
8994
"@opentelemetry/resources": "^2.0.0",
9095
"@opentelemetry/sdk-logs": "^0.208.0",
9196
"@opentelemetry/semantic-conventions": "^1.28.0",
92-
"@agentclientprotocol/sdk": "^0.14.0",
93-
"@anthropic-ai/claude-agent-sdk": "0.2.12",
94-
"@anthropic-ai/sdk": "^0.71.0",
95-
"@hono/node-server": "^1.19.9",
96-
"@modelcontextprotocol/sdk": "^1.25.3",
9797
"@types/jsonwebtoken": "^9.0.10",
9898
"commander": "^14.0.2",
9999
"diff": "^8.0.2",

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
137137
this.checkAuthStatus();
138138

139139
const meta = params._meta as NewSessionMeta | undefined;
140-
const internalSessionId = uuidv7();
140+
const sessionId = uuidv7();
141141
const permissionMode: TwigExecutionMode =
142142
meta?.permissionMode &&
143143
TWIG_EXECUTION_MODES.includes(meta.permissionMode as TwigExecutionMode)
@@ -151,11 +151,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
151151
cwd: params.cwd,
152152
mcpServers,
153153
permissionMode,
154-
canUseTool: this.createCanUseTool(internalSessionId),
154+
canUseTool: this.createCanUseTool(sessionId),
155155
logger: this.logger,
156156
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
157157
userProvidedOptions: meta?.claudeCode?.options,
158-
onModeChange: this.createOnModeChange(internalSessionId),
158+
sessionId,
159+
isResume: false,
160+
onModeChange: this.createOnModeChange(sessionId),
159161
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
160162
onProcessExited: this.processCallbacks?.onProcessExited,
161163
});
@@ -164,29 +166,35 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
164166
const q = query({ prompt: input, options });
165167

166168
const session = this.createSession(
167-
internalSessionId,
169+
sessionId,
168170
q,
169171
input,
170172
permissionMode,
171173
params.cwd,
172174
options.abortController as AbortController,
173175
);
174176
session.taskRunId = meta?.taskRunId;
175-
this.registerPersistence(
176-
internalSessionId,
177-
meta as Record<string, unknown>,
178-
);
177+
this.registerPersistence(sessionId, meta as Record<string, unknown>);
178+
179+
if (meta?.taskRunId) {
180+
await this.client.extNotification("_posthog/sdk_session", {
181+
taskRunId: meta.taskRunId,
182+
sessionId,
183+
adapter: "claude",
184+
});
185+
}
186+
179187
const modelOptions = await this.getModelConfigOptions();
180188
session.modelId = modelOptions.currentModelId;
181189
await this.trySetModel(q, modelOptions.currentModelId);
182190

183191
this.sendAvailableCommandsUpdate(
184-
internalSessionId,
192+
sessionId,
185193
await getAvailableSlashCommands(q),
186194
);
187195

188196
return {
189-
sessionId: internalSessionId,
197+
sessionId,
190198
configOptions: await this.buildConfigOptions(modelOptions),
191199
};
192200
}
@@ -198,12 +206,15 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
198206
async resumeSession(
199207
params: LoadSessionRequest,
200208
): Promise<LoadSessionResponse> {
201-
const { sessionId: internalSessionId } = params;
202-
if (this.sessionId === internalSessionId) {
209+
const meta = params._meta as NewSessionMeta | undefined;
210+
const sessionId = meta?.sessionId;
211+
if (!sessionId) {
212+
throw new Error("Cannot resume session without sessionId");
213+
}
214+
if (this.sessionId === sessionId) {
203215
return {};
204216
}
205217

206-
const meta = params._meta as NewSessionMeta | undefined;
207218
const mcpServers = parseMcpServers(params);
208219
await fetchMcpToolMetadata(mcpServers, this.logger);
209220

@@ -214,27 +225,21 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
214225
: "default";
215226

216227
const { query: q, session } = await this.initializeQuery({
217-
internalSessionId,
218228
cwd: params.cwd,
219229
permissionMode,
220230
mcpServers,
221231
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
222232
userProvidedOptions: meta?.claudeCode?.options,
223-
sessionId: meta?.sessionId,
233+
sessionId,
234+
isResume: true,
224235
additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
225236
});
226237

227238
session.taskRunId = meta?.taskRunId;
228-
if (meta?.sessionId) {
229-
session.sessionId = meta.sessionId;
230-
}
231239

232-
this.registerPersistence(
233-
internalSessionId,
234-
meta as Record<string, unknown>,
235-
);
240+
this.registerPersistence(sessionId, meta as Record<string, unknown>);
236241
this.sendAvailableCommandsUpdate(
237-
internalSessionId,
242+
sessionId,
238243
await getAvailableSlashCommands(q),
239244
);
240245

@@ -322,13 +327,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
322327
}
323328

324329
private async initializeQuery(config: {
325-
internalSessionId: string;
326330
cwd: string;
327331
permissionMode: TwigExecutionMode;
328332
mcpServers: ReturnType<typeof parseMcpServers>;
329333
userProvidedOptions?: Options;
330334
systemPrompt?: Options["systemPrompt"];
331-
sessionId?: string;
335+
sessionId: string;
336+
isResume: boolean;
332337
additionalDirectories?: string[];
333338
}): Promise<{
334339
query: Query;
@@ -341,13 +346,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
341346
cwd: config.cwd,
342347
mcpServers: config.mcpServers,
343348
permissionMode: config.permissionMode,
344-
canUseTool: this.createCanUseTool(config.internalSessionId),
349+
canUseTool: this.createCanUseTool(config.sessionId),
345350
logger: this.logger,
346351
systemPrompt: config.systemPrompt,
347352
userProvidedOptions: config.userProvidedOptions,
348353
sessionId: config.sessionId,
354+
isResume: config.isResume,
349355
additionalDirectories: config.additionalDirectories,
350-
onModeChange: this.createOnModeChange(config.internalSessionId),
356+
onModeChange: this.createOnModeChange(config.sessionId),
351357
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
352358
onProcessExited: this.processCallbacks?.onProcessExited,
353359
});
@@ -356,7 +362,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
356362
const abortController = options.abortController as AbortController;
357363

358364
const session = this.createSession(
359-
config.internalSessionId,
365+
config.sessionId,
360366
q,
361367
input,
362368
config.permissionMode,
@@ -596,6 +602,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
596602

597603
case "tool_progress":
598604
case "auth_status":
605+
case "tool_use_summary":
599606
return null;
600607

601608
default:

packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -328,20 +328,10 @@ export async function handleSystemMessage(
328328
message: any,
329329
context: MessageHandlerContext,
330330
): Promise<void> {
331-
const { session, sessionId, client, logger } = context;
331+
const { sessionId, client, logger } = context;
332332

333333
switch (message.subtype) {
334334
case "init":
335-
if (message.session_id && session && !session.sessionId) {
336-
session.sessionId = message.session_id;
337-
if (session.taskRunId) {
338-
await client.extNotification("_posthog/sdk_session", {
339-
taskRunId: session.taskRunId,
340-
sessionId: message.session_id,
341-
adapter: "claude",
342-
});
343-
}
344-
}
345335
break;
346336
case "compact_boundary":
347337
await client.extNotification("_posthog/compact_boundary", {

packages/agent/src/adapters/claude/session/options.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export interface BuildOptionsParams {
2727
logger: Logger;
2828
systemPrompt?: Options["systemPrompt"];
2929
userProvidedOptions?: Options;
30-
sessionId?: string;
30+
sessionId: string;
31+
isResume: boolean;
3132
additionalDirectories?: string[];
3233
onModeChange?: OnModeChange;
3334
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
@@ -213,7 +214,7 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
213214
),
214215
...(params.onProcessSpawned && {
215216
spawnClaudeCodeProcess: buildSpawnWrapper(
216-
params.sessionId ?? "unknown",
217+
params.sessionId,
217218
params.onProcessSpawned,
218219
params.onProcessExited,
219220
),
@@ -224,8 +225,11 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
224225
options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
225226
}
226227

227-
if (params.sessionId) {
228+
if (params.isResume) {
228229
options.resume = params.sessionId;
230+
options.forkSession = false;
231+
} else {
232+
options.sessionId = params.sessionId;
229233
}
230234

231235
if (params.additionalDirectories) {

packages/agent/src/adapters/claude/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export type Session = BaseSession & {
2929
modelId?: string;
3030
cwd: string;
3131
taskRunId?: string;
32-
sessionId?: string;
3332
lastPlanFilePath?: string;
3433
lastPlanContent?: string;
3534
};

0 commit comments

Comments
 (0)