Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/many-guests-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---

add parent session id when creating a session
58 changes: 56 additions & 2 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -190,6 +195,55 @@ export class CLI {

return result
},
getParentTaskId: async (taskId: string) => {
const result = await (async () => {
// 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<TaskHistoryData>((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
})()

logs.debug(`Resolved parent task ID for task ${taskId}: "${result}"`, "SessionManager")

return result || undefined
},
})
logs.debug("SessionManager initialized with dependencies", "CLI")

Expand Down
2 changes: 2 additions & 0 deletions src/shared/kilocode/cli-sessions/core/SessionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/shared/kilocode/cli-sessions/core/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface SessionManagerDependencies extends TrpcClientDependencies {
getOrganizationId: (taskId: string) => Promise<string | undefined>
getMode: (taskId: string) => Promise<string | undefined>
getModel: (taskId: string) => Promise<string | undefined>
getParentTaskId: (taskId: string) => Promise<string | undefined>
}

/**
Expand All @@ -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

Expand Down Expand Up @@ -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,
})
Expand Down
7 changes: 7 additions & 0 deletions src/shared/kilocode/cli-sessions/core/SessionSyncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface SessionSyncServiceDependencies {
getOrganizationId: (taskId: string) => Promise<string | undefined>
getMode: (taskId: string) => Promise<string | undefined>
getModel: (taskId: string) => Promise<string | undefined>
getParentTaskId: (taskId: string) => Promise<string | undefined>
onSessionCreated?: (message: SessionCreatedMessage) => void
onSessionSynced?: (message: SessionSyncedMessage) => void
}
Expand Down Expand Up @@ -81,6 +82,7 @@ export class SessionSyncService {
private readonly getOrganizationId: (taskId: string) => Promise<string | undefined>
private readonly getMode: (taskId: string) => Promise<string | undefined>
private readonly getModel: (taskId: string) => Promise<string | undefined>
private readonly getParentTaskId: (taskId: string) => Promise<string | undefined>
private readonly onSessionCreated: (message: SessionCreatedMessage) => void
private readonly onSessionSynced: (message: SessionSyncedMessage) => void

Expand All @@ -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 ?? (() => {})
}
Expand Down Expand Up @@ -383,6 +386,9 @@ export class SessionSyncService {
): Promise<string> {
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,
Expand All @@ -391,6 +397,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe("SessionManager", () => {
let mockGetOrganizationId: any
let mockGetMode: any
let mockGetModel: any
let mockGetParentTaskId: any

beforeEach(() => {
vi.clearAllMocks()
Expand Down Expand Up @@ -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", () => {
Expand All @@ -145,6 +147,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}

const instance = SessionManager.init(dependencies)
Expand Down Expand Up @@ -177,6 +180,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}
const firstInstance = SessionManager.init(dependencies)

Expand Down Expand Up @@ -207,6 +211,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}
SessionManager.init(dependencies)
})
Expand Down Expand Up @@ -257,6 +262,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}
manager = SessionManager.init(dependencies)
})
Expand Down Expand Up @@ -370,6 +376,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}

SessionManager.init(dependencies)
Expand Down Expand Up @@ -398,6 +405,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}

SessionManager.init(dependencies)
Expand All @@ -418,6 +426,7 @@ describe("SessionManager", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
}

SessionManager.init(dependencies)
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => ({
Expand Down Expand Up @@ -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()

Expand All @@ -101,6 +101,7 @@ describe("SessionSyncService", () => {
getOrganizationId: mockGetOrganizationId,
getMode: mockGetMode,
getModel: mockGetModel,
getParentTaskId: mockGetParentTaskId,
onSessionCreated: mockOnSessionCreated,
onSessionSynced: mockOnSessionSynced,
})
Expand Down Expand Up @@ -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",
Expand All @@ -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,
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ export function kilo_initializeSessionManager({

logger.debug(`Resolved model for task ${taskId}: "${result}"`, "SessionManager")

return result || undefined
},
getParentTaskId: async (taskId: string) => {
const result = await (async () => {
const currentTask = provider.getCurrentTask()

if (currentTask?.taskId === taskId) {
return currentTask.parentTaskId
}

const task = await provider.getTaskWithId(taskId)

return task?.historyItem?.parentTaskId
})()

logger.debug(`Resolved parent task ID for task ${taskId}: "${result}"`, "SessionManager")

return result || undefined
},
})
Expand Down