mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
♻️ refactor(hetero-agent): rename ccSessionId to heteroSessionId (#13961)
CC-specific naming leaked into a field/module that's meant to be shared
across heterogeneous agent adapters. Rename to a provider-neutral id so
new adapters can reuse the topic-level session binding without inheriting
CC terminology.
- ChatTopicMetadata.ccSessionId -> heteroSessionId
- resolveCcResume / CcResumeDecision -> resolveHeteroResume / HeteroResumeDecision
- ccResume.{ts,test.ts} -> heteroResume.{ts,test.ts}
- updateTopicMetadata zod schema + executor + conversationLifecycle callsites
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc9164ae4a
commit
30e93ada67
6 changed files with 48 additions and 44 deletions
|
|
@ -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 <sessionId>` to continue the conversation.
|
||||
* CC CLI stores sessions per-cwd under `~/.claude/projects/<encoded-cwd>/`,
|
||||
* 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 <sessionId>`).
|
||||
* CC CLI stores sessions per-cwd under `~/.claude/projects/<encoded-cwd>/`,
|
||||
* so resume requires the current cwd to equal `workingDirectory`.
|
||||
*/
|
||||
heteroSessionId?: string;
|
||||
model?: string;
|
||||
/**
|
||||
* Free-form feedback collected after agent onboarding completion.
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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' }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<encoded-cwd>/`, 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/<encoded-cwd>/`, 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 ?? '';
|
||||
|
||||
|
|
@ -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/<encoded-cwd>/`),
|
||||
// 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/<encoded-cwd>/`), 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 ?? '',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue