diff --git a/packages/types/src/topic/topic.ts b/packages/types/src/topic/topic.ts index 17da528b25..927cc3639d 100644 --- a/packages/types/src/topic/topic.ts +++ b/packages/types/src/topic/topic.ts @@ -56,18 +56,18 @@ export interface OnboardingSessionSnapshot { export interface ChatTopicMetadata { bot?: ChatTopicBotContext; boundDeviceId?: string; - /** - * CC session ID for multi-turn resume (desktop only). - * Persisted after each CC execution so the next message in the same topic - * can use `--resume ` to continue the conversation. - * CC CLI stores sessions per-cwd under `~/.claude/projects//`, - * so resume requires the current cwd to equal `workingDirectory`. - */ - ccSessionId?: string; /** * Cron job ID that triggered this topic creation (if created by scheduled task) */ cronJobId?: string; + /** + * Persistent session id for a heterogeneous agent (desktop only). + * Saved after each turn so the next message in the same topic can resume + * the conversation (e.g. Claude Code CLI uses `--resume `). + * CC CLI stores sessions per-cwd under `~/.claude/projects//`, + * so resume requires the current cwd to equal `workingDirectory`. + */ + heteroSessionId?: string; model?: string; /** * Free-form feedback collected after agent onboarding completion. diff --git a/src/server/routers/lambda/topic.ts b/src/server/routers/lambda/topic.ts index 3855695380..9a44284a80 100644 --- a/src/server/routers/lambda/topic.ts +++ b/src/server/routers/lambda/topic.ts @@ -557,7 +557,7 @@ export const topicRouter = router({ id: z.string(), metadata: z.object({ boundDeviceId: z.string().optional(), - ccSessionId: z.string().optional(), + heteroSessionId: z.string().optional(), model: z.string().optional(), onboardingFeedback: z .object({ diff --git a/src/store/chat/slices/aiChat/actions/__tests__/ccResume.test.ts b/src/store/chat/slices/aiChat/actions/__tests__/heteroResume.test.ts similarity index 71% rename from src/store/chat/slices/aiChat/actions/__tests__/ccResume.test.ts rename to src/store/chat/slices/aiChat/actions/__tests__/heteroResume.test.ts index 375452abf7..f5ae6931fb 100644 --- a/src/store/chat/slices/aiChat/actions/__tests__/ccResume.test.ts +++ b/src/store/chat/slices/aiChat/actions/__tests__/heteroResume.test.ts @@ -1,16 +1,16 @@ import type { ChatTopicMetadata } from '@lobechat/types'; import { describe, expect, it } from 'vitest'; -import { resolveCcResume } from '../ccResume'; +import { resolveHeteroResume } from '../heteroResume'; -describe('resolveCcResume', () => { +describe('resolveHeteroResume', () => { it('resumes when saved cwd matches current cwd', () => { const metadata: ChatTopicMetadata = { - ccSessionId: 'session-123', + heteroSessionId: 'session-123', workingDirectory: '/Users/me/projA', }; - expect(resolveCcResume(metadata, '/Users/me/projA')).toEqual({ + expect(resolveHeteroResume(metadata, '/Users/me/projA')).toEqual({ cwdChanged: false, resumeSessionId: 'session-123', }); @@ -18,11 +18,11 @@ describe('resolveCcResume', () => { it('skips resume when saved cwd differs from current cwd', () => { const metadata: ChatTopicMetadata = { - ccSessionId: 'session-123', + heteroSessionId: 'session-123', workingDirectory: '/Users/me/projA', }; - expect(resolveCcResume(metadata, '/Users/me/projB')).toEqual({ + expect(resolveHeteroResume(metadata, '/Users/me/projB')).toEqual({ cwdChanged: true, resumeSessionId: undefined, }); @@ -30,11 +30,11 @@ describe('resolveCcResume', () => { it('treats undefined current cwd as empty string (matches saved empty cwd)', () => { const metadata: ChatTopicMetadata = { - ccSessionId: 'session-123', + heteroSessionId: 'session-123', workingDirectory: '', }; - expect(resolveCcResume(metadata, undefined)).toEqual({ + expect(resolveHeteroResume(metadata, undefined)).toEqual({ cwdChanged: false, resumeSessionId: 'session-123', }); @@ -42,11 +42,11 @@ describe('resolveCcResume', () => { it('flags mismatch when saved cwd is non-empty but current cwd is undefined', () => { const metadata: ChatTopicMetadata = { - ccSessionId: 'session-123', + heteroSessionId: 'session-123', workingDirectory: '/Users/me/projA', }; - expect(resolveCcResume(metadata, undefined)).toEqual({ + expect(resolveHeteroResume(metadata, undefined)).toEqual({ cwdChanged: true, resumeSessionId: undefined, }); @@ -57,24 +57,24 @@ describe('resolveCcResume', () => { // Passing the stale id through was the original bug — reset instead, and // let the next turn rebuild the session with a recorded cwd. const metadata: ChatTopicMetadata = { - ccSessionId: 'legacy-session', + heteroSessionId: 'legacy-session', }; - expect(resolveCcResume(metadata, '/Users/me/any')).toEqual({ + expect(resolveHeteroResume(metadata, '/Users/me/any')).toEqual({ cwdChanged: true, resumeSessionId: undefined, }); }); it('returns no session when nothing is stored', () => { - expect(resolveCcResume({}, '/Users/me/projA')).toEqual({ + expect(resolveHeteroResume({}, '/Users/me/projA')).toEqual({ cwdChanged: false, resumeSessionId: undefined, }); }); it('handles undefined metadata', () => { - expect(resolveCcResume(undefined, '/Users/me/projA')).toEqual({ + expect(resolveHeteroResume(undefined, '/Users/me/projA')).toEqual({ cwdChanged: false, resumeSessionId: undefined, }); @@ -87,7 +87,7 @@ describe('resolveCcResume', () => { workingDirectory: '/Users/me/projA', }; - expect(resolveCcResume(metadata, '/Users/me/projB')).toEqual({ + expect(resolveHeteroResume(metadata, '/Users/me/projB')).toEqual({ cwdChanged: false, resumeSessionId: undefined, }); diff --git a/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts b/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts index 265252c7b3..d2128464d2 100644 --- a/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +++ b/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts @@ -26,7 +26,7 @@ import { messageService } from '@/services/message'; import { getAgentStoreState } from '@/store/agent'; import { agentByIdSelectors, agentSelectors } from '@/store/agent/selectors'; import { agentGroupByIdSelectors, getChatGroupStoreState } from '@/store/agentGroup'; -import { resolveCcResume } from '@/store/chat/slices/aiChat/actions/ccResume'; +import { resolveHeteroResume } from '@/store/chat/slices/aiChat/actions/heteroResume'; import { type ChatStore } from '@/store/chat/store'; import { createPendingCompressedGroup, @@ -444,14 +444,17 @@ export class ConversationLifecycleActionImpl { const userMsg = heteroData.messages.find((m: any) => m.id === heteroData.userMessageId); const persistedImageList = userMsg?.imageList; - // Read CC session ID from topic metadata for multi-turn resume. - // `resolveCcResume` drops the sessionId when the saved cwd doesn't - // match the current one, so CC doesn't emit + // Read heterogeneous-agent session id from topic metadata for multi-turn + // resume. `resolveHeteroResume` drops the sessionId when the saved cwd + // doesn't match the current one, so CC doesn't emit // "No conversation found with session ID". const topic = heteroContext.topicId ? topicSelectors.getTopicById(heteroContext.topicId)(this.#get()) : undefined; - const { cwdChanged, resumeSessionId } = resolveCcResume(topic?.metadata, workingDirectory); + const { cwdChanged, resumeSessionId } = resolveHeteroResume( + topic?.metadata, + workingDirectory, + ); if (cwdChanged) { antdMessage.info(t('heteroAgent.resumeReset.cwdChanged', { ns: 'chat' })); } diff --git a/src/store/chat/slices/aiChat/actions/ccResume.ts b/src/store/chat/slices/aiChat/actions/heteroResume.ts similarity index 63% rename from src/store/chat/slices/aiChat/actions/ccResume.ts rename to src/store/chat/slices/aiChat/actions/heteroResume.ts index d50f2e5bce..b1f728162e 100644 --- a/src/store/chat/slices/aiChat/actions/ccResume.ts +++ b/src/store/chat/slices/aiChat/actions/heteroResume.ts @@ -1,17 +1,17 @@ import type { ChatTopicMetadata } from '@lobechat/types'; -export interface CcResumeDecision { +export interface HeteroResumeDecision { /** True when a saved cwd exists and disagrees with the current cwd. */ cwdChanged: boolean; - /** Session ID to pass to `--resume`, or undefined when resume must be skipped. */ + /** Session id to resume with, or undefined when resume must be skipped. */ resumeSessionId: string | undefined; } /** - * Decide whether we can safely resume a prior Claude Code session for the - * current turn. CC CLI stores sessions per-cwd under - * `~/.claude/projects//`, so resuming from a different cwd - * blows up with "No conversation found with session ID". + * Decide whether we can safely resume a prior heterogeneous-agent session for + * the current turn. Claude Code CLI (the current consumer) stores sessions + * per-cwd under `~/.claude/projects//`, so resuming from a + * different cwd blows up with "No conversation found with session ID". * * Strict rule: only resume when the topic's bound `workingDirectory` is * present AND equals the current cwd. Legacy topics (sessionId present, @@ -19,11 +19,11 @@ export interface CcResumeDecision { * and silently passing a stale id is exactly what caused the original * failure. */ -export const resolveCcResume = ( +export const resolveHeteroResume = ( metadata: ChatTopicMetadata | undefined, currentWorkingDirectory: string | undefined, -): CcResumeDecision => { - const savedSessionId = metadata?.ccSessionId; +): HeteroResumeDecision => { + const savedSessionId = metadata?.heteroSessionId; const savedCwd = metadata?.workingDirectory; const cwd = currentWorkingDirectory ?? ''; diff --git a/src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts b/src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts index 49fd68b037..de7094e040 100644 --- a/src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts +++ b/src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts @@ -645,14 +645,15 @@ export const executeHeterogeneousAgent = async ( // Send the prompt — blocks until process exits await heterogeneousAgentService.sendPrompt(agentSessionId, message, imageList); - // Persist CC session ID + the cwd it was created under, for multi-turn - // resume. CC stores sessions per-cwd (`~/.claude/projects//`), - // so the next turn must verify the cwd hasn't changed before `--resume`. - // Reuses `workingDirectory` as the topic-level binding — pinning the - // topic to this cwd once CC has executed here. + // Persist heterogeneous-agent session id + the cwd it was created under, + // for multi-turn resume. CC stores sessions per-cwd + // (`~/.claude/projects//`), so the next turn must verify the + // cwd hasn't changed before `--resume`. Reuses `workingDirectory` as the + // topic-level binding — pinning the topic to this cwd once the agent has + // executed here. if (adapter.sessionId && context.topicId) { get().updateTopicMetadata(context.topicId, { - ccSessionId: adapter.sessionId, + heteroSessionId: adapter.sessionId, workingDirectory: workingDirectory ?? '', }); }