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
This commit is contained in:
程序员阿江(Relakkes) 2026-07-11 04:08:43 +08:00
parent d6cd0f1fc7
commit 936d970751
4 changed files with 56 additions and 1 deletions

View File

@ -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()

View File

@ -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' }
: {}),

View File

@ -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<string> {
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<string, unknown>)
.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 = {

View File

@ -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 {