From 936d97075178e32bc6fc7f0b0dbbf445d0097635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 11 Jul 2026 04:08:43 +0800 Subject: [PATCH] fix(server): expose desktop sessions to Claude resume #988 Stamp desktop-owned transcripts with the claude-desktop entrypoint while preserving the SDK runtime entrypoint used for provider authentication. Tests: focused regression 41/41; chat contract 240/240; server 1622/1622; coverage 5/5 with changed lines 4/4. Live-path proof: hermetic loopback transcript was visible in Claude Code 2.1.206 /resume; no real user credentials or config were used. Constraint: check:impact remains policy-blocked until a PR has the allow-cli-core-change label. Confidence: high Scope-risk: narrow --- .../__tests__/conversation-service.test.ts | 1 + src/server/services/conversationService.ts | 3 ++ .../__tests__/sessionStorageFlush.test.ts | 48 +++++++++++++++++++ src/utils/sessionStorage.ts | 5 +- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/server/__tests__/conversation-service.test.ts b/src/server/__tests__/conversation-service.test.ts index 108c143a..d95edeb0 100644 --- a/src/server/__tests__/conversation-service.test.ts +++ b/src/server/__tests__/conversation-service.test.ts @@ -486,6 +486,7 @@ describe('ConversationService', () => { expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('kimi-k2.6') expect(env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBe('1') expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0') + expect(env.CC_HAHA_TRANSCRIPT_ENTRYPOINT).toBe('claude-desktop') expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined() expect(env.CC_HAHA_TRACE_PROVIDER_ID).toBeUndefined() expect(env.CC_HAHA_TRACE_PROVIDER_NAME).toBeUndefined() diff --git a/src/server/services/conversationService.ts b/src/server/services/conversationService.ts index 7688e19a..1776e332 100644 --- a/src/server/services/conversationService.ts +++ b/src/server/services/conversationService.ts @@ -1206,6 +1206,9 @@ export class ConversationService { // should come from Desktop-managed config or inherited launch env, not // be reintroduced from the repo's .env file. CC_HAHA_SKIP_DOTENV: '1', + // Keep the SDK runtime identity for auth and client behavior, but stamp + // desktop-owned transcripts with an entrypoint visible to Claude /resume. + CC_HAHA_TRANSCRIPT_ENTRYPOINT: 'claude-desktop', ...(explicitProviderEnv ? { CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1' } : {}), diff --git a/src/utils/__tests__/sessionStorageFlush.test.ts b/src/utils/__tests__/sessionStorageFlush.test.ts index 9f2615a4..be39e8f8 100644 --- a/src/utils/__tests__/sessionStorageFlush.test.ts +++ b/src/utils/__tests__/sessionStorageFlush.test.ts @@ -6,11 +6,18 @@ import * as path from 'node:path' import { enqueueSessionEntryAfterPendingForTesting, flushSessionStorage, + getTranscriptPathForSession, + recordTranscript, resetProjectForTesting, } from '../sessionStorage.js' +import { switchSession } from '../../bootstrap/state.js' +import type { SessionId } from '../../types/ids.js' import type { CustomTitleMessage } from '../../types/logs.js' const originalConfigDir = process.env.CLAUDE_CONFIG_DIR +const originalTranscriptEntrypoint = process.env.CC_HAHA_TRANSCRIPT_ENTRYPOINT +const originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT +const originalTestPersistence = process.env.TEST_ENABLE_SESSION_PERSISTENCE async function createTmpDir(): Promise { const dir = path.join( @@ -27,6 +34,7 @@ describe('sessionStorage flush', () => { beforeEach(async () => { tmpDir = await createTmpDir() process.env.CLAUDE_CONFIG_DIR = tmpDir + process.env.TEST_ENABLE_SESSION_PERSISTENCE = '1' resetProjectForTesting() }) @@ -37,9 +45,49 @@ describe('sessionStorage flush', () => { } else { process.env.CLAUDE_CONFIG_DIR = originalConfigDir } + if (originalTranscriptEntrypoint === undefined) { + delete process.env.CC_HAHA_TRANSCRIPT_ENTRYPOINT + } else { + process.env.CC_HAHA_TRANSCRIPT_ENTRYPOINT = originalTranscriptEntrypoint + } + if (originalEntrypoint === undefined) { + delete process.env.CLAUDE_CODE_ENTRYPOINT + } else { + process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint + } + if (originalTestPersistence === undefined) { + delete process.env.TEST_ENABLE_SESSION_PERSISTENCE + } else { + process.env.TEST_ENABLE_SESSION_PERSISTENCE = originalTestPersistence + } await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}) }) + it('records the desktop transcript entrypoint without changing the runtime entrypoint', async () => { + const sessionId = '22222222-2222-4222-8222-222222222222' + switchSession(sessionId as SessionId) + process.env.CLAUDE_CODE_ENTRYPOINT = 'sdk-cli' + process.env.CC_HAHA_TRANSCRIPT_ENTRYPOINT = 'claude-desktop' + resetProjectForTesting() + + await recordTranscript([{ + type: 'user', + uuid: '33333333-3333-4333-8333-333333333333', + message: { role: 'user', content: 'desktop resume visibility' }, + } as never]) + await flushSessionStorage() + + const transcript = await fs.readFile(getTranscriptPathForSession(sessionId), 'utf-8') + const userEntry = transcript + .trim() + .split('\n') + .map((line) => JSON.parse(line) as Record) + .find((entry) => entry.type === 'user') + + expect(userEntry?.entrypoint).toBe('claude-desktop') + expect(process.env.CLAUDE_CODE_ENTRYPOINT).toBe('sdk-cli') + }) + it('drains writes that are queued by pending operations during flush', async () => { const transcriptPath = path.join(tmpDir, 'late-enqueue.jsonl') const entry: CustomTitleMessage = { diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index dad2db70..5dd7e941 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -421,7 +421,10 @@ export function getUserType(): string { } function getEntrypoint(): string | undefined { - return process.env.CLAUDE_CODE_ENTRYPOINT + return ( + process.env.CC_HAHA_TRANSCRIPT_ENTRYPOINT?.trim() || + process.env.CLAUDE_CODE_ENTRYPOINT + ) } export function isCustomTitleEnabled(): boolean {