diff --git a/.changeset/many-guests-crash.md b/.changeset/many-guests-crash.md new file mode 100644 index 0000000000..bf47f61249 --- /dev/null +++ b/.changeset/many-guests-crash.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"kilo-code": minor +--- + +add parent session id when creating a session diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 738cb3fa2f..18ea37b1c7 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -11,9 +11,14 @@ import { loadConfigAtom, mappedExtensionStateAtom, providersAtom, saveConfigAtom import { ciExitReasonAtom } from "./state/atoms/ci.js" import { requestRouterModelsAtom } from "./state/atoms/actions.js" import { loadHistoryAtom } from "./state/atoms/history.js" -import { taskHistoryDataAtom, updateTaskHistoryFiltersAtom } from "./state/atoms/taskHistory.js" +import { + addPendingRequestAtom, + TaskHistoryData, + taskHistoryDataAtom, + updateTaskHistoryFiltersAtom, +} from "./state/atoms/taskHistory.js" import { sendWebviewMessageAtom } from "./state/atoms/actions.js" -import { taskResumedViaContinueOrSessionAtom } from "./state/atoms/extension.js" +import { taskResumedViaContinueOrSessionAtom, currentTaskAtom } from "./state/atoms/extension.js" import { getTelemetryService, getIdentityManager } from "./services/telemetry/index.js" import { notificationsAtom, notificationsErrorAtom, notificationsLoadingAtom } from "./state/atoms/notifications.js" import { fetchKilocodeNotifications } from "./utils/notifications.js" @@ -190,6 +195,59 @@ export class CLI { return result }, + getParentTaskId: async (taskId: string) => { + const result = await (async () => { + try { + // Check if the current task matches the taskId + const currentTask = this.store?.get(currentTaskAtom) + + if (currentTask?.id === taskId) { + return currentTask.parentTaskId + } + + // Otherwise, fetch the task from history using promise-based request/response pattern + const requestId = Date.now().toString() + + // Create a promise that will be resolved when the response arrives + const responsePromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Task history request timed out")) + }, 5000) // 5 second timeout as fallback + + this.store?.set(addPendingRequestAtom, { + requestId, + resolve, + reject, + timeout, + }) + }) + + // Send task history request to get the specific task + await this.store?.set(sendWebviewMessageAtom, { + type: "taskHistoryRequest", + payload: { + requestId, + workspace: "current", + sort: "newest", + favoritesOnly: false, + pageIndex: 0, + }, + }) + + // Wait for the actual response (not a timer) + const taskHistoryData = await responsePromise + const task = taskHistoryData.historyItems.find((item) => item.id === taskId) + + return task?.parentTaskId + } catch { + return undefined + } + })() + + logs.debug(`Resolved parent task ID for task ${taskId}: "${result}"`, "SessionManager") + + return result || undefined + }, }) logs.debug("SessionManager initialized with dependencies", "CLI") diff --git a/src/shared/kilocode/cli-sessions/core/SessionClient.ts b/src/shared/kilocode/cli-sessions/core/SessionClient.ts index abddb78e5b..3fa8008103 100644 --- a/src/shared/kilocode/cli-sessions/core/SessionClient.ts +++ b/src/shared/kilocode/cli-sessions/core/SessionClient.ts @@ -12,6 +12,7 @@ export interface Session { organization_id: string | null last_mode: string | null last_model: string | null + parent_session_id: string | null } export interface SessionWithSignedUrls extends Session { @@ -36,6 +37,7 @@ export interface CreateSessionInput { last_mode?: string | null | undefined last_model?: string | null | undefined organization_id?: string | undefined + parent_session_id?: string | null | undefined } export type CreateSessionOutput = Session @@ -123,7 +125,7 @@ export class SessionClient { * Create a new session */ async create(input: CreateSessionInput): Promise { - return await this.trpcClient.request("cliSessions.create", "POST", { + return await this.trpcClient.request("cliSessions.createV2", "POST", { ...input, created_on_platform: process.env.KILO_PLATFORM || input.created_on_platform, }) diff --git a/src/shared/kilocode/cli-sessions/core/SessionManager.ts b/src/shared/kilocode/cli-sessions/core/SessionManager.ts index 0220ec405e..912beabe03 100644 --- a/src/shared/kilocode/cli-sessions/core/SessionManager.ts +++ b/src/shared/kilocode/cli-sessions/core/SessionManager.ts @@ -48,6 +48,7 @@ export interface SessionManagerDependencies extends TrpcClientDependencies { getOrganizationId: (taskId: string) => Promise getMode: (taskId: string) => Promise getModel: (taskId: string) => Promise + getParentTaskId: (taskId: string) => Promise } /** @@ -70,8 +71,9 @@ export class SessionManager { * 0 - No versioning, some sessions incomplete * 1 - Initial version * 2 - Added organization id, last mode and last model + * 3 - Added parent session id */ - static readonly VERSION = 2 + static readonly VERSION = 3 private static instance: SessionManager | null = null @@ -148,6 +150,7 @@ export class SessionManager { getOrganizationId: dependencies.getOrganizationId, getMode: dependencies.getMode, getModel: dependencies.getModel, + getParentTaskId: dependencies.getParentTaskId, onSessionCreated: dependencies.onSessionCreated, onSessionSynced: dependencies.onSessionSynced, }) diff --git a/src/shared/kilocode/cli-sessions/core/SessionSyncService.ts b/src/shared/kilocode/cli-sessions/core/SessionSyncService.ts index 2d7532f396..a7cdd8a298 100644 --- a/src/shared/kilocode/cli-sessions/core/SessionSyncService.ts +++ b/src/shared/kilocode/cli-sessions/core/SessionSyncService.ts @@ -49,6 +49,7 @@ export interface SessionSyncServiceDependencies { getOrganizationId: (taskId: string) => Promise getMode: (taskId: string) => Promise getModel: (taskId: string) => Promise + getParentTaskId: (taskId: string) => Promise onSessionCreated?: (message: SessionCreatedMessage) => void onSessionSynced?: (message: SessionSyncedMessage) => void } @@ -81,6 +82,7 @@ export class SessionSyncService { private readonly getOrganizationId: (taskId: string) => Promise private readonly getMode: (taskId: string) => Promise private readonly getModel: (taskId: string) => Promise + private readonly getParentTaskId: (taskId: string) => Promise private readonly onSessionCreated: (message: SessionCreatedMessage) => void private readonly onSessionSynced: (message: SessionSyncedMessage) => void @@ -100,6 +102,7 @@ export class SessionSyncService { this.getOrganizationId = dependencies.getOrganizationId this.getMode = dependencies.getMode this.getModel = dependencies.getModel + this.getParentTaskId = dependencies.getParentTaskId this.onSessionCreated = dependencies.onSessionCreated ?? (() => {}) this.onSessionSynced = dependencies.onSessionSynced ?? (() => {}) } @@ -362,6 +365,7 @@ export class SessionSyncService { ...basePayload, last_mode: currentMode, last_model: currentModel, + version: this.version, }) this.stateManager.updateTimestamp(sessionId, updateResult.updated_at) @@ -383,6 +387,9 @@ export class SessionSyncService { ): Promise { const currentMode = await this.getMode(taskId) const currentModel = await this.getModel(taskId) + const parentTaskId = await this.getParentTaskId(taskId) + + const parentSessionId = parentTaskId ? this.persistenceManager.getSessionForTask(parentTaskId) : undefined const createdSession = await this.sessionClient.create({ ...basePayload, @@ -391,6 +398,7 @@ export class SessionSyncService { organization_id: await this.getOrganizationId(taskId), last_mode: currentMode, last_model: currentModel, + parent_session_id: parentSessionId, }) const sessionId = createdSession.session_id diff --git a/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts b/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts index e6fa6c666d..c6d4685edf 100644 --- a/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts +++ b/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts @@ -46,6 +46,7 @@ describe("SessionManager", () => { let mockGetOrganizationId: any let mockGetMode: any let mockGetModel: any + let mockGetParentTaskId: any beforeEach(() => { vi.clearAllMocks() @@ -129,6 +130,7 @@ describe("SessionManager", () => { mockGetOrganizationId = vi.fn().mockResolvedValue("org-123") mockGetMode = vi.fn().mockResolvedValue("code") mockGetModel = vi.fn().mockResolvedValue("gpt-4") + mockGetParentTaskId = vi.fn().mockResolvedValue(undefined) }) describe("Singleton Pattern", () => { @@ -145,6 +147,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } const instance = SessionManager.init(dependencies) @@ -177,6 +180,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } const firstInstance = SessionManager.init(dependencies) @@ -207,6 +211,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } SessionManager.init(dependencies) }) @@ -257,6 +262,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } manager = SessionManager.init(dependencies) }) @@ -370,6 +376,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } SessionManager.init(dependencies) @@ -398,6 +405,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } SessionManager.init(dependencies) @@ -418,6 +426,7 @@ describe("SessionManager", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, } SessionManager.init(dependencies) diff --git a/src/shared/kilocode/cli-sessions/core/__tests__/SessionSyncService.spec.ts b/src/shared/kilocode/cli-sessions/core/__tests__/SessionSyncService.spec.ts index 9de390032b..0e54428532 100644 --- a/src/shared/kilocode/cli-sessions/core/__tests__/SessionSyncService.spec.ts +++ b/src/shared/kilocode/cli-sessions/core/__tests__/SessionSyncService.spec.ts @@ -1,7 +1,6 @@ import { promises as fs } from "fs" import { SessionSyncService } from "../SessionSyncService" import type { ILogger } from "../../types/ILogger.js" -import type { ClineMessage } from "@roo-code/types" // Mock fs vi.mock("fs", () => ({ @@ -78,6 +77,7 @@ const mockSyncQueue = { const mockGetOrganizationId = vi.fn() const mockGetMode = vi.fn() const mockGetModel = vi.fn() +const mockGetParentTaskId = vi.fn() const mockOnSessionCreated = vi.fn() const mockOnSessionSynced = vi.fn() @@ -101,6 +101,7 @@ describe("SessionSyncService", () => { getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, + getParentTaskId: mockGetParentTaskId, onSessionCreated: mockOnSessionCreated, onSessionSynced: mockOnSessionSynced, }) @@ -475,6 +476,7 @@ describe("SessionSyncService", () => { mockGetOrganizationId.mockResolvedValue("org-1") mockGetMode.mockResolvedValue("code") mockGetModel.mockResolvedValue("gpt-4") + mockGetParentTaskId.mockResolvedValue(undefined) mockSessionClient.create.mockResolvedValue({ session_id: "new-session-1", updated_at: "2023-01-01T10:00:00Z", @@ -488,6 +490,62 @@ describe("SessionSyncService", () => { organization_id: "org-1", last_mode: "code", last_model: "gpt-4", + parent_session_id: undefined, + }) + }) + + it("creates new session with parent session ID when parent task exists", async () => { + mockGitStateService.getGitState.mockResolvedValue(null) + mockPersistenceManager.getSessionForTask.mockImplementation((taskId) => { + if (taskId === "parent-task-1") return "parent-session-1" + return undefined + }) + mockGetOrganizationId.mockResolvedValue("org-1") + mockGetMode.mockResolvedValue("code") + mockGetModel.mockResolvedValue("gpt-4") + mockGetParentTaskId.mockResolvedValue("parent-task-1") + mockSessionClient.create.mockResolvedValue({ + session_id: "new-session-1", + updated_at: "2023-01-01T10:00:00Z", + }) + + await service.doSync() + + expect(mockSessionClient.create).toHaveBeenCalledWith({ + created_on_platform: "test-platform", + version: 1, + organization_id: "org-1", + last_mode: "code", + last_model: "gpt-4", + parent_session_id: "parent-session-1", + }) + }) + + it("creates new session with parent_session_id undefined when parent task has no session", async () => { + mockGitStateService.getGitState.mockResolvedValue(null) + mockPersistenceManager.getSessionForTask.mockImplementation((taskId) => { + // Parent task exists but has no session + if (taskId === "parent-task-1") return undefined + return undefined + }) + mockGetOrganizationId.mockResolvedValue("org-1") + mockGetMode.mockResolvedValue("code") + mockGetModel.mockResolvedValue("gpt-4") + mockGetParentTaskId.mockResolvedValue("parent-task-1") + mockSessionClient.create.mockResolvedValue({ + session_id: "new-session-1", + updated_at: "2023-01-01T10:00:00Z", + }) + + await service.doSync() + + expect(mockSessionClient.create).toHaveBeenCalledWith({ + created_on_platform: "test-platform", + version: 1, + organization_id: "org-1", + last_mode: "code", + last_model: "gpt-4", + parent_session_id: undefined, }) }) diff --git a/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts b/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts index c01a3414d8..70967d0956 100644 --- a/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts +++ b/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts @@ -122,6 +122,27 @@ export function kilo_initializeSessionManager({ logger.debug(`Resolved model for task ${taskId}: "${result}"`, "SessionManager") + return result || undefined + }, + getParentTaskId: async (taskId: string) => { + const result = await (async () => { + try { + const currentTask = provider.getCurrentTask() + + if (currentTask?.taskId === taskId) { + return currentTask.parentTaskId + } + + const task = await provider.getTaskWithId(taskId, false) + + return task?.historyItem?.parentTaskId + } catch { + return undefined + } + })() + + logger.debug(`Resolved parent task ID for task ${taskId}: "${result}"`, "SessionManager") + return result || undefined }, })