| @@ -61,6 +61,7 @@ type cursorSession struct { · 60 unmodified lines |
| 61 | 61 | pending []pendingMcpExec |
| 62 | 62 | cancel context.CancelFunc // cancels the session-scoped heartbeat (NOT tied to HTTP request) |
| 63 | 63 | createdAt time.Time |
| 64 | + authID string // auth file ID that created this session (for multi-account isolation) |
| 64 | 65 | toolResultCh chan []toolResultInfo // receives tool results from the next HTTP request |
| 65 | 66 | resumeOutCh chan cliproxyexecutor.StreamChunk // output channel for resumed response |
| 66 | 67 | switchOutput func(ch chan cliproxyexecutor.StreamChunk) // callback to switch output channel |
| @@ -320,10 +321,12 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 253 unmodified lines |
| 320 | 321 | parsed.Model, len(parsed.UserText), len(parsed.Turns), len(parsed.Tools), len(parsed.ToolResults)) |
| 321 | 322 | |
| 322 | 323 | conversationId := deriveConversationId(apiKeyFromContext(ctx), ccSessionId, parsed.SystemPrompt) |
| 323 | | − log.Debugf("cursor: conversationId=%s ccSessionId=%s", conversationId, ccSessionId) |
| 324 | + authID := auth.ID // e.g. "cursor.json" or "cursor-account2.json" |
| 325 | + log.Debugf("cursor: conversationId=%s authID=%s", conversationId, authID) |
| 324 | 326 | |
| 325 | | − // Use conversationId as session key — stable across requests in the same Claude Code session |
| 326 | | − sessionKey := conversationId |
| 327 | + // Include authID in keys for multi-account isolation |
| 328 | + sessionKey := authID + ":" + conversationId |
| 329 | + checkpointKey := sessionKey // same isolation |
| 327 | 330 | needsTranslate := from.String() != "" && from.String() != "openai" |
| 328 | 331 | |
| 329 | 332 | // Check if we can resume an existing session with tool results |
| @@ -335,10 +338,13 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 5 unmodified lines |
| 335 | 338 | } |
| 336 | 339 | e.mu.Unlock() |
| 337 | 340 | |
| 338 | | − if hasSession && session.stream != nil { |
| 341 | + if hasSession && session.stream != nil && session.authID == authID { |
| 339 | 342 | log.Debugf("cursor: resuming session %s with %d tool results", sessionKey, len(parsed.ToolResults)) |
| 340 | 343 | return e.resumeWithToolResults(ctx, session, parsed, from, to, req, originalPayload, payload, needsTranslate) |
| 341 | 344 | } |
| 345 | + if hasSession && session.authID != authID { |
| 346 | + log.Warnf("cursor: session %s belongs to auth %s, but request is from %s — skipping resume", sessionKey, session.authID, authID) |
| 347 | + } |
| 342 | 348 | } |
| 343 | 349 | |
| 344 | 350 | // Clean up any stale session for this key |
| @@ -349,15 +355,15 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 4 unmodified lines |
| 349 | 355 | } |
| 350 | 356 | e.mu.Unlock() |
| 351 | 357 | |
| 352 | | − // Look up saved checkpoint for this conversation |
| 358 | + // Look up saved checkpoint for this conversation + account |
| 353 | 359 | e.mu.Lock() |
| 354 | | − saved, hasCheckpoint := e.checkpoints[conversationId] |
| 360 | + saved, hasCheckpoint := e.checkpoints[checkpointKey] |
| 355 | 361 | e.mu.Unlock() |
| 356 | 362 | |
| 357 | 363 | params := buildRunRequestParams(parsed, conversationId) |
| 358 | 364 | |
| 359 | 365 | if hasCheckpoint && saved.data != nil { |
| 360 | | − log.Debugf("cursor: using saved checkpoint (%d bytes) for conversationId=%s", len(saved.data), conversationId) |
| 366 | + log.Debugf("cursor: using saved checkpoint (%d bytes) for key=%s", len(saved.data), checkpointKey) |
| 361 | 367 | params.RawCheckpoint = saved.data |
| 362 | 368 | // Merge saved blobStore into params |
| 363 | 369 | if params.BlobStore == nil { |
| @@ -507,6 +513,7 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 143 unmodified lines |
| 507 | 513 | pending: []pendingMcpExec{exec}, |
| 508 | 514 | cancel: sessionCancel, |
| 509 | 515 | createdAt: time.Now(), |
| 516 | + authID: authID, |
| 510 | 517 | toolResultCh: toolResultCh, // reuse same channel across rounds |
| 511 | 518 | resumeOutCh: resumeOut, |
| 512 | 519 | switchOutput: func(ch chan cliproxyexecutor.StreamChunk) { |
| @@ -532,13 +539,13 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 19 unmodified lines |
| 532 | 539 | func(cpData []byte) { |
| 533 | 540 | // Save checkpoint for this conversation |
| 534 | 541 | e.mu.Lock() |
| 535 | | − e.checkpoints[conversationId] = &savedCheckpoint{ |
| 542 | + e.checkpoints[checkpointKey] = &savedCheckpoint{ |
| 536 | 543 | data: cpData, |
| 537 | 544 | blobStore: params.BlobStore, |
| 538 | 545 | updatedAt: time.Now(), |
| 539 | 546 | } |
| 540 | 547 | e.mu.Unlock() |
| 541 | | − log.Debugf("cursor: saved checkpoint (%d bytes) for conversationId=%s", len(cpData), conversationId) |
| 548 | + log.Debugf("cursor: saved checkpoint (%d bytes) for key=%s", len(cpData), checkpointKey) |
| 542 | 549 | }, |
| 543 | 550 | ) |
| 544 | 551 | |