🐛 fix(cc): persist workingDirectory when CC topic is created (#13956)

Hetero-agent topic creation went through `aiChat.sendMessageInServer`'s
`newTopic` payload, which had no metadata field, so the topic row was
inserted with `metadata.workingDirectory = NULL`. Today the only writer
is the post-execution `updateTopicMetadata` in `heterogeneousAgentExecutor`
— that never lands when CC is cancelled or errors before completion, and
in the meantime the topic is missed by By-Project grouping and `--resume`
cwd verification has nothing to compare against.

Source the cwd at the start of the hetero branch and thread it through
`newTopic.metadata`, so the binding is set at insert time. The post-exec
update still runs to record `ccSessionId` (and is now a no-op for cwd).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu 2026-04-18 23:08:15 +08:00 committed by GitHub
parent 30e93ada67
commit f38dcc4cfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 19 additions and 3 deletions

View file

@ -8,7 +8,7 @@ import { PageSelectionSchema } from './message/ui/params';
import type { OpenAIChatMessage } from './openai/chat';
import type { LobeUniformTool } from './tool';
import { LobeUniformToolSchema } from './tool';
import type { ChatTopic } from './topic';
import type { ChatTopic, ChatTopicMetadata } from './topic';
import type { ChatThreadType } from './topic/thread';
import { ThreadType } from './topic/thread';
@ -66,6 +66,14 @@ export interface SendMessageServerParams {
*/
newThread?: CreateThreadWithMessageParams;
newTopic?: {
/**
* Topic metadata persisted at creation time. For CC/heterogeneous
* agents this carries `workingDirectory` so the topic is bound to a
* project from the moment it's created (used by By-Project grouping
* and CC `--resume` cwd verification), instead of waiting for the
* post-execution metadata write which can be skipped on cancel/error.
*/
metadata?: ChatTopicMetadata;
title?: string;
topicMessageIds?: string[];
};
@ -111,6 +119,7 @@ export const AiSendMessageServerSchema = z.object({
newThread: CreateThreadWithMessageSchema.optional(),
newTopic: z
.object({
metadata: z.custom<ChatTopicMetadata>().optional(),
title: z.string().optional(),
topicMessageIds: z.array(z.string()).optional(),
})

View file

@ -85,6 +85,7 @@ export const aiChatRouter = router({
agentId: input.agentId,
groupId: input.groupId,
messages: input.newTopic.topicMessageIds,
metadata: input.newTopic.metadata,
sessionId,
title: input.newTopic.title,
});

View file

@ -347,6 +347,13 @@ export class ConversationLifecycleActionImpl {
const agentConfig = agentSelectors.getAgentConfigById(agentId)(getAgentStoreState());
const heterogeneousProvider = agentConfig?.agencyConfig?.heterogeneousProvider;
if (isDesktop && heterogeneousProvider?.type === 'claude-code') {
// Resolve cwd up-front so the new topic is bound to a project at
// creation time. Otherwise the row stays NULL until the post-execution
// metadata write — which never lands on cancel/error and meanwhile
// makes By-Project grouping miss the topic and `--resume` unsafe.
const workingDirectory =
agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(getAgentStoreState());
// Persist messages to DB first (same as client mode)
let heteroData: SendMessageServerResponse | undefined;
try {
@ -358,6 +365,7 @@ export class ConversationLifecycleActionImpl {
newAssistantMessage: { model, provider: 'claude-code' },
newTopic: !operationContext.topicId
? {
metadata: workingDirectory ? { workingDirectory } : undefined,
title: message.slice(0, 20) || t('defaultTitle', { ns: 'topic' }),
topicMessageIds: messages.map((m) => m.id),
}
@ -437,8 +445,6 @@ export class ConversationLifecycleActionImpl {
try {
const { executeHeterogeneousAgent } = await import('./heterogeneousAgentExecutor');
const workingDirectory =
agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(getAgentStoreState());
// Extract imageList from the persisted user message (chatUploadFileList
// may already be cleared by this point, so we read from DB instead)
const userMsg = heteroData.messages.find((m: any) => m.id === heteroData.userMessageId);