|
|
@@ -47,12 +47,13 @@ export class SessionManager {
|
|
|
static readonly SYNC_INTERVAL = 3000
|
|
|
static readonly MAX_PATCH_SIZE_BYTES = 5 * 1024 * 1024
|
|
|
static readonly VERSION = 1
|
|
|
+ static readonly QUEUE_FLUSH_THRESHOLD = 5
|
|
|
|
|
|
private static instance = new SessionManager()
|
|
|
|
|
|
static init(dependencies?: SessionManagerDependencies) {
|
|
|
if (dependencies) {
|
|
|
- SessionManager.instance.initSingleton(dependencies)
|
|
|
+ SessionManager.instance.initDeps(dependencies)
|
|
|
}
|
|
|
|
|
|
return SessionManager.instance
|
|
|
@@ -64,15 +65,13 @@ export class SessionManager {
|
|
|
private sessionTitles: Record<string, string> = {}
|
|
|
private sessionUpdatedAt: Record<string, string> = {}
|
|
|
private tokenValid: Record<string, boolean | undefined> = {}
|
|
|
+ private verifiedSessions: Set<string> = new Set()
|
|
|
|
|
|
public get sessionId() {
|
|
|
return this.lastActiveSessionId || this.sessionPersistenceManager?.getLastSession()?.sessionId
|
|
|
}
|
|
|
private lastActiveSessionId: string | null = null
|
|
|
|
|
|
- private timer: NodeJS.Timeout | null = null
|
|
|
- private isSyncing: boolean = false
|
|
|
-
|
|
|
private pathProvider: IPathProvider | undefined
|
|
|
private logger: ILogger | undefined
|
|
|
private extensionMessenger: IExtensionMessenger | undefined
|
|
|
@@ -108,24 +107,6 @@ export class SessionManager {
|
|
|
|
|
|
private pendingSync: Promise<void> | null = null
|
|
|
|
|
|
- private initSingleton(dependencies: SessionManagerDependencies) {
|
|
|
- this.initDeps(dependencies)
|
|
|
-
|
|
|
- if (!this.timer) {
|
|
|
- this.timer = setInterval(async () => {
|
|
|
- if (this.pendingSync) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- this.pendingSync = this.syncSession()
|
|
|
-
|
|
|
- await this.pendingSync
|
|
|
-
|
|
|
- this.pendingSync = null
|
|
|
- }, SessionManager.SYNC_INTERVAL)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private queue = [] as {
|
|
|
taskId: string
|
|
|
blobName: string
|
|
|
@@ -144,6 +125,10 @@ export class SessionManager {
|
|
|
timestamp: Date.now(),
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+ if (this.queue.length > SessionManager.QUEUE_FLUSH_THRESHOLD) {
|
|
|
+ this.doSync()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
setWorkspaceDirectory(dir: string) {
|
|
|
@@ -196,8 +181,6 @@ export class SessionManager {
|
|
|
throw new Error("SessionManager used before initialization")
|
|
|
}
|
|
|
|
|
|
- this.isSyncing = true
|
|
|
-
|
|
|
const session = (await this.sessionClient.get({
|
|
|
session_id: sessionId,
|
|
|
include_blob_urls: true,
|
|
|
@@ -301,6 +284,7 @@ export class SessionManager {
|
|
|
|
|
|
this.sessionPersistenceManager.setSessionForTask(historyItem.id, sessionId)
|
|
|
this.lastActiveSessionId = sessionId
|
|
|
+ this.verifiedSessions.add(sessionId)
|
|
|
|
|
|
await this.extensionMessenger.sendWebviewMessage({
|
|
|
type: "addTaskToHistory",
|
|
|
@@ -333,8 +317,6 @@ export class SessionManager {
|
|
|
if (rethrowError) {
|
|
|
throw error
|
|
|
}
|
|
|
- } finally {
|
|
|
- this.isSyncing = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -404,6 +386,39 @@ export class SessionManager {
|
|
|
|
|
|
let sessionId = this.sessionPersistenceManager.getSessionForTask(taskId)
|
|
|
|
|
|
+ if (sessionId) {
|
|
|
+ if (!this.verifiedSessions.has(sessionId)) {
|
|
|
+ this.logger?.debug("Verifying session existence", "SessionManager", { taskId, sessionId })
|
|
|
+
|
|
|
+ try {
|
|
|
+ const session = await this.sessionClient.get({
|
|
|
+ session_id: sessionId,
|
|
|
+ include_blob_urls: false,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!session) {
|
|
|
+ this.logger?.info("Session no longer exists, will create new session", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ sessionId,
|
|
|
+ })
|
|
|
+ sessionId = undefined
|
|
|
+ } else {
|
|
|
+ this.verifiedSessions.add(sessionId)
|
|
|
+ this.logger?.debug("Session verified and cached", "SessionManager", { taskId, sessionId })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.info("Session verification failed, will create new session", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ sessionId,
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
+ })
|
|
|
+ sessionId = undefined
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.logger?.debug("Session already verified (cached)", "SessionManager", { taskId, sessionId })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (!sessionId) {
|
|
|
this.logger?.debug("No existing session for task, creating new session", "SessionManager", { taskId })
|
|
|
|
|
|
@@ -431,6 +446,8 @@ export class SessionManager {
|
|
|
this.logger?.debug("Uploaded conversation blobs to session", "SessionManager", { sessionId })
|
|
|
|
|
|
this.sessionPersistenceManager.setSessionForTask(taskId, sessionId)
|
|
|
+
|
|
|
+ this.verifiedSessions.add(sessionId)
|
|
|
} else {
|
|
|
this.logger?.debug("Found existing session for task", "SessionManager", { taskId, sessionId })
|
|
|
}
|
|
|
@@ -446,11 +463,6 @@ export class SessionManager {
|
|
|
}
|
|
|
|
|
|
private async syncSession() {
|
|
|
- if (this.isSyncing) {
|
|
|
- this.logger?.debug("Sync already in progress, skipping", "SessionManager")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
if (this.queue.length === 0) {
|
|
|
return
|
|
|
}
|
|
|
@@ -466,336 +478,379 @@ export class SessionManager {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- try {
|
|
|
- this.isSyncing = true
|
|
|
-
|
|
|
- const token = await this.getToken?.()
|
|
|
+ const token = await this.getToken?.()
|
|
|
|
|
|
- if (!token) {
|
|
|
- this.logger?.debug("No token available for session sync, skipping", "SessionManager")
|
|
|
- return
|
|
|
- }
|
|
|
+ if (!token) {
|
|
|
+ this.logger?.debug("No token available for session sync, skipping", "SessionManager")
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- if (this.tokenValid[token] === undefined) {
|
|
|
- this.logger?.debug("Checking token validity", "SessionManager")
|
|
|
+ if (this.tokenValid[token] === undefined) {
|
|
|
+ this.logger?.debug("Checking token validity", "SessionManager")
|
|
|
|
|
|
+ try {
|
|
|
const tokenValid = await this.sessionClient.tokenValid()
|
|
|
|
|
|
this.tokenValid[token] = tokenValid
|
|
|
-
|
|
|
- this.logger?.debug("Token validity checked", "SessionManager", { tokenValid })
|
|
|
- }
|
|
|
-
|
|
|
- if (!this.tokenValid[token]) {
|
|
|
- this.logger?.debug("Token is invalid, skipping sync", "SessionManager")
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.error("Failed to check token validity", "SessionManager", {
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
+ })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- const taskIds = new Set<string>(this.queue.map((item) => item.taskId))
|
|
|
- const lastItem = this.queue[this.queue.length - 1]
|
|
|
+ this.logger?.debug("Token validity checked", "SessionManager", { tokenValid: this.tokenValid[token] })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.tokenValid[token]) {
|
|
|
+ this.logger?.debug("Token is invalid, skipping sync", "SessionManager")
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- this.logger?.debug("Starting session sync", "SessionManager", {
|
|
|
- queueLength: this.queue.length,
|
|
|
- taskCount: taskIds.size,
|
|
|
+ const taskIds = new Set<string>(this.queue.map((item) => item.taskId))
|
|
|
+ const lastItem = this.queue[this.queue.length - 1]
|
|
|
+
|
|
|
+ this.logger?.debug("Starting session sync", "SessionManager", {
|
|
|
+ queueLength: this.queue.length,
|
|
|
+ taskCount: taskIds.size,
|
|
|
+ })
|
|
|
+
|
|
|
+ let gitInfo: Awaited<ReturnType<typeof this.getGitState>> | null = null
|
|
|
+ try {
|
|
|
+ gitInfo = await this.getGitState()
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.debug("Could not get git state", "SessionManager", {
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
})
|
|
|
+ }
|
|
|
|
|
|
- let gitInfo: Awaited<ReturnType<typeof this.getGitState>> | null = null
|
|
|
+ for (const taskId of taskIds) {
|
|
|
try {
|
|
|
- gitInfo = await this.getGitState()
|
|
|
- } catch (error) {
|
|
|
- this.logger?.debug("Could not get git state", "SessionManager", {
|
|
|
- error: error instanceof Error ? error.message : String(error),
|
|
|
+ const taskItems = this.queue.filter((item) => item.taskId === taskId)
|
|
|
+ const reversedTaskItems = [...taskItems].reverse()
|
|
|
+
|
|
|
+ this.logger?.debug("Processing task", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ itemCount: taskItems.length,
|
|
|
})
|
|
|
- }
|
|
|
|
|
|
- for (const taskId of taskIds) {
|
|
|
- try {
|
|
|
- const taskItems = this.queue.filter((item) => item.taskId === taskId)
|
|
|
- const reversedTaskItems = [...taskItems].reverse()
|
|
|
+ const basePayload: Partial<Parameters<NonNullable<typeof this.sessionClient>["create"]>[0]> = {}
|
|
|
|
|
|
- this.logger?.debug("Processing task", "SessionManager", {
|
|
|
- taskId,
|
|
|
- itemCount: taskItems.length,
|
|
|
- })
|
|
|
+ if (gitInfo?.repoUrl) {
|
|
|
+ basePayload.git_url = gitInfo.repoUrl
|
|
|
+ }
|
|
|
|
|
|
- const basePayload: Partial<Parameters<NonNullable<typeof this.sessionClient>["create"]>[0]> = {}
|
|
|
+ let sessionId = this.sessionPersistenceManager.getSessionForTask(taskId)
|
|
|
|
|
|
- if (gitInfo?.repoUrl) {
|
|
|
- basePayload.git_url = gitInfo.repoUrl
|
|
|
- }
|
|
|
+ if (sessionId) {
|
|
|
+ this.logger?.debug("Found existing session for task", "SessionManager", { taskId, sessionId })
|
|
|
|
|
|
- let sessionId = this.sessionPersistenceManager.getSessionForTask(taskId)
|
|
|
+ const gitUrlChanged = !!gitInfo?.repoUrl && gitInfo.repoUrl !== this.taskGitUrls[taskId]
|
|
|
|
|
|
- if (sessionId) {
|
|
|
- this.logger?.debug("Found existing session for task", "SessionManager", { taskId, sessionId })
|
|
|
+ if (gitUrlChanged && gitInfo?.repoUrl) {
|
|
|
+ this.taskGitUrls[taskId] = gitInfo.repoUrl
|
|
|
|
|
|
- const gitUrlChanged = !!gitInfo?.repoUrl && gitInfo.repoUrl !== this.taskGitUrls[taskId]
|
|
|
+ this.logger?.debug("Git URL changed, updating session", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ newGitUrl: gitInfo.repoUrl,
|
|
|
+ })
|
|
|
|
|
|
- if (gitUrlChanged && gitInfo?.repoUrl) {
|
|
|
- this.taskGitUrls[taskId] = gitInfo.repoUrl
|
|
|
+ const updateResult = await this.sessionClient.update({
|
|
|
+ session_id: sessionId,
|
|
|
+ ...basePayload,
|
|
|
+ })
|
|
|
|
|
|
- this.logger?.debug("Git URL changed, updating session", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- newGitUrl: gitInfo.repoUrl,
|
|
|
- })
|
|
|
+ this.updateSessionTimestamp(sessionId, updateResult.updated_at)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.logger?.debug("Creating new session for task", "SessionManager", { taskId })
|
|
|
|
|
|
- const updateResult = await this.sessionClient.update({
|
|
|
- session_id: sessionId,
|
|
|
- ...basePayload,
|
|
|
- })
|
|
|
+ const createdSession = await this.sessionClient.create({
|
|
|
+ ...basePayload,
|
|
|
+ created_on_platform: this.platform,
|
|
|
+ version: SessionManager.VERSION,
|
|
|
+ })
|
|
|
|
|
|
- this.updateSessionTimestamp(sessionId, updateResult.updated_at)
|
|
|
- }
|
|
|
- } else {
|
|
|
- this.logger?.debug("Creating new session for task", "SessionManager", { taskId })
|
|
|
+ sessionId = createdSession.session_id
|
|
|
|
|
|
- const createdSession = await this.sessionClient.create({
|
|
|
- ...basePayload,
|
|
|
- created_on_platform: this.platform,
|
|
|
- version: SessionManager.VERSION,
|
|
|
- })
|
|
|
+ this.logger?.info("Created new session", "SessionManager", { taskId, sessionId })
|
|
|
|
|
|
- sessionId = createdSession.session_id
|
|
|
+ this.sessionPersistenceManager.setSessionForTask(taskId, createdSession.session_id)
|
|
|
|
|
|
- this.logger?.info("Created new session", "SessionManager", { taskId, sessionId })
|
|
|
+ this.onSessionCreated?.({
|
|
|
+ timestamp: Date.now(),
|
|
|
+ event: "session_created",
|
|
|
+ sessionId: createdSession.session_id,
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- this.sessionPersistenceManager.setSessionForTask(taskId, createdSession.session_id)
|
|
|
+ if (!sessionId) {
|
|
|
+ this.logger?.warn("No session ID available after create/get, skipping task", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
- this.onSessionCreated?.({
|
|
|
- timestamp: Date.now(),
|
|
|
- event: "session_created",
|
|
|
- sessionId: createdSession.session_id,
|
|
|
- })
|
|
|
- }
|
|
|
+ const blobNames = new Set(taskItems.map((item) => item.blobName))
|
|
|
+ const blobUploads: Promise<unknown>[] = []
|
|
|
|
|
|
- if (!sessionId) {
|
|
|
- this.logger?.warn("No session ID available after create/get, skipping task", "SessionManager", {
|
|
|
+ this.logger?.debug("Uploading blobs for session", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ blobNames: Array.from(blobNames),
|
|
|
+ })
|
|
|
+
|
|
|
+ for (const blobName of blobNames) {
|
|
|
+ const lastBlobItem = reversedTaskItems.find((item) => item.blobName === blobName)
|
|
|
+
|
|
|
+ if (!lastBlobItem) {
|
|
|
+ this.logger?.warn("Could not find blob item in reversed list", "SessionManager", {
|
|
|
+ blobName,
|
|
|
taskId,
|
|
|
})
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- const blobNames = new Set(taskItems.map((item) => item.blobName))
|
|
|
- const blobUploads: Promise<unknown>[] = []
|
|
|
+ const fileContents = JSON.parse(readFileSync(lastBlobItem.blobPath, "utf-8"))
|
|
|
|
|
|
- this.logger?.debug("Uploading blobs for session", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- blobNames: Array.from(blobNames),
|
|
|
- })
|
|
|
+ blobUploads.push(
|
|
|
+ this.sessionClient
|
|
|
+ .uploadBlob(
|
|
|
+ sessionId,
|
|
|
+ lastBlobItem.blobName as Parameters<typeof this.sessionClient.uploadBlob>[1],
|
|
|
+ fileContents,
|
|
|
+ )
|
|
|
+ .then((result) => {
|
|
|
+ this.logger?.debug("Blob uploaded successfully", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ blobName,
|
|
|
+ })
|
|
|
|
|
|
- for (const blobName of blobNames) {
|
|
|
- const lastBlobItem = reversedTaskItems.find((item) => item.blobName === blobName)
|
|
|
+ // Track the updated_at timestamp from the upload using high-water mark
|
|
|
+ this.updateSessionTimestamp(sessionId, result.updated_at)
|
|
|
|
|
|
- if (!lastBlobItem) {
|
|
|
- this.logger?.warn("Could not find blob item in reversed list", "SessionManager", {
|
|
|
- blobName,
|
|
|
- taskId,
|
|
|
- })
|
|
|
- continue
|
|
|
- }
|
|
|
+ for (let i = 0; i < this.queue.length; i++) {
|
|
|
+ const item = this.queue[i]
|
|
|
|
|
|
- const fileContents = JSON.parse(readFileSync(lastBlobItem.blobPath, "utf-8"))
|
|
|
+ if (!item) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
- blobUploads.push(
|
|
|
- this.sessionClient
|
|
|
- .uploadBlob(
|
|
|
+ if (
|
|
|
+ item.blobName === blobName &&
|
|
|
+ item.taskId === taskId &&
|
|
|
+ item.timestamp <= lastBlobItem.timestamp
|
|
|
+ ) {
|
|
|
+ this.queue.splice(i, 1)
|
|
|
+ i--
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ this.logger?.error("Failed to upload blob", "SessionManager", {
|
|
|
sessionId,
|
|
|
- lastBlobItem.blobName as Parameters<typeof this.sessionClient.uploadBlob>[1],
|
|
|
- fileContents,
|
|
|
- )
|
|
|
- .then((result) => {
|
|
|
- this.logger?.debug("Blob uploaded successfully", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- blobName,
|
|
|
- })
|
|
|
+ blobName,
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ )
|
|
|
|
|
|
- // Track the updated_at timestamp from the upload using high-water mark
|
|
|
- this.updateSessionTimestamp(sessionId, result.updated_at)
|
|
|
+ if (blobName !== "ui_messages" || this.sessionTitles[sessionId]) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
- for (let i = 0; i < this.queue.length; i++) {
|
|
|
- const item = this.queue[i]
|
|
|
-
|
|
|
- if (!item) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if (
|
|
|
- item.blobName === blobName &&
|
|
|
- item.taskId === taskId &&
|
|
|
- item.timestamp <= lastBlobItem.timestamp
|
|
|
- ) {
|
|
|
- this.queue.splice(i, 1)
|
|
|
- i--
|
|
|
- }
|
|
|
- }
|
|
|
+ this.logger?.debug("Checking for session title generation", "SessionManager", { sessionId })
|
|
|
+
|
|
|
+ void (async () => {
|
|
|
+ try {
|
|
|
+ if (!this.sessionClient) {
|
|
|
+ this.logger?.warn("Session client not initialized", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
})
|
|
|
- .catch((error) => {
|
|
|
- this.logger?.error("Failed to upload blob", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- blobName,
|
|
|
- error: error instanceof Error ? error.message : String(error),
|
|
|
- })
|
|
|
- }),
|
|
|
- )
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- if (blobName === "ui_messages" && !this.sessionTitles[sessionId]) {
|
|
|
- this.logger?.debug("Checking for session title generation", "SessionManager", { sessionId })
|
|
|
+ this.sessionTitles[sessionId] = "Pending title"
|
|
|
|
|
|
- void (async () => {
|
|
|
- try {
|
|
|
- if (!this.sessionClient) {
|
|
|
- this.logger?.warn("Session client not initialized", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
+ const session = await this.sessionClient.get({ session_id: sessionId })
|
|
|
+
|
|
|
+ if (session.title) {
|
|
|
+ this.sessionTitles[sessionId] = session.title
|
|
|
|
|
|
- this.sessionTitles[sessionId] = "Pending title"
|
|
|
+ this.logger?.debug("Found existing session title", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ title: session.title,
|
|
|
+ })
|
|
|
|
|
|
- const session = await this.sessionClient.get({ session_id: sessionId })
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- if (session.title) {
|
|
|
- this.sessionTitles[sessionId] = session.title
|
|
|
+ const generatedTitle = await this.generateTitle(fileContents)
|
|
|
|
|
|
- this.logger?.debug("Found existing session title", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- title: session.title,
|
|
|
- })
|
|
|
+ if (!generatedTitle) {
|
|
|
+ throw new Error("Failed to generate session title")
|
|
|
+ }
|
|
|
|
|
|
- return
|
|
|
- }
|
|
|
+ const updateResult = await this.sessionClient.update({
|
|
|
+ session_id: sessionId,
|
|
|
+ title: generatedTitle,
|
|
|
+ })
|
|
|
|
|
|
- const generatedTitle = await this.generateTitle(fileContents)
|
|
|
+ this.sessionTitles[sessionId] = generatedTitle
|
|
|
+ this.updateSessionTimestamp(sessionId, updateResult.updated_at)
|
|
|
|
|
|
- if (!generatedTitle) {
|
|
|
- throw new Error("Failed to generate session title")
|
|
|
- }
|
|
|
+ this.logger?.debug("Updated session title", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ generatedTitle,
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.error("Failed to generate session title", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
+ })
|
|
|
|
|
|
- const updateResult = await this.sessionClient.update({
|
|
|
- session_id: sessionId,
|
|
|
- title: generatedTitle,
|
|
|
- })
|
|
|
+ const localTitle = this.getFirstMessageText(fileContents as ClineMessage[], true) || ""
|
|
|
|
|
|
- this.sessionTitles[sessionId] = generatedTitle
|
|
|
- this.updateSessionTimestamp(sessionId, updateResult.updated_at)
|
|
|
+ if (!localTitle) {
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- this.logger?.debug("Updated session title", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- generatedTitle,
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- this.logger?.error("Failed to generate session title", "SessionManager", {
|
|
|
+ try {
|
|
|
+ await this.renameSession(sessionId, localTitle)
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.error(
|
|
|
+ "Failed to update session title using local title",
|
|
|
+ "SessionManager",
|
|
|
+ {
|
|
|
sessionId,
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
- })
|
|
|
-
|
|
|
- this.sessionTitles[sessionId] = ""
|
|
|
- }
|
|
|
- })()
|
|
|
+ },
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ })()
|
|
|
+ }
|
|
|
|
|
|
- if (gitInfo) {
|
|
|
- const gitStateData = {
|
|
|
- head: gitInfo.head,
|
|
|
- patch: gitInfo.patch,
|
|
|
- branch: gitInfo.branch,
|
|
|
- }
|
|
|
+ if (gitInfo) {
|
|
|
+ const gitStateData = {
|
|
|
+ head: gitInfo.head,
|
|
|
+ patch: gitInfo.patch,
|
|
|
+ branch: gitInfo.branch,
|
|
|
+ }
|
|
|
|
|
|
- const gitStateHash = this.hashGitState(gitStateData)
|
|
|
+ const gitStateHash = this.hashGitState(gitStateData)
|
|
|
|
|
|
- if (gitStateHash !== this.taskGitHashes[taskId]) {
|
|
|
- this.logger?.debug("Git state changed, uploading", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- head: gitInfo.head?.substring(0, 8),
|
|
|
- })
|
|
|
+ if (gitStateHash === this.taskGitHashes[taskId]) {
|
|
|
+ this.logger?.debug("Git state unchanged, skipping upload", "SessionManager", { sessionId })
|
|
|
+ } else {
|
|
|
+ this.logger?.debug("Git state changed, uploading", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ head: gitInfo.head?.substring(0, 8),
|
|
|
+ })
|
|
|
|
|
|
- this.taskGitHashes[taskId] = gitStateHash
|
|
|
+ this.taskGitHashes[taskId] = gitStateHash
|
|
|
|
|
|
- blobUploads.push(
|
|
|
- this.sessionClient
|
|
|
- .uploadBlob(sessionId, "git_state", gitStateData)
|
|
|
- .then((result) => {
|
|
|
- // Track the updated_at timestamp from git state upload using high-water mark
|
|
|
- this.updateSessionTimestamp(sessionId, result.updated_at)
|
|
|
+ blobUploads.push(
|
|
|
+ this.sessionClient
|
|
|
+ .uploadBlob(sessionId, "git_state", gitStateData)
|
|
|
+ .then((result) => {
|
|
|
+ // Track the updated_at timestamp from git state upload using high-water mark
|
|
|
+ this.updateSessionTimestamp(sessionId, result.updated_at)
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ this.logger?.error("Failed to upload git state", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
})
|
|
|
- .catch((error) => {
|
|
|
- this.logger?.error("Failed to upload git state", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- error: error instanceof Error ? error.message : String(error),
|
|
|
- })
|
|
|
- }),
|
|
|
- )
|
|
|
- } else {
|
|
|
- this.logger?.debug("Git state unchanged, skipping upload", "SessionManager", { sessionId })
|
|
|
- }
|
|
|
+ }),
|
|
|
+ )
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- await Promise.all(blobUploads)
|
|
|
+ await Promise.all(blobUploads)
|
|
|
|
|
|
- this.logger?.debug("Completed blob uploads for task", "SessionManager", {
|
|
|
- taskId,
|
|
|
+ this.logger?.debug("Completed blob uploads for task", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ sessionId,
|
|
|
+ uploadCount: blobUploads.length,
|
|
|
+ })
|
|
|
+
|
|
|
+ // Emit session synced event with the latest updated_at timestamp
|
|
|
+ const latestUpdatedAt = this.sessionUpdatedAt[sessionId]
|
|
|
+ if (latestUpdatedAt) {
|
|
|
+ const updatedAtTimestamp = new Date(latestUpdatedAt).getTime()
|
|
|
+ this.onSessionSynced?.({
|
|
|
sessionId,
|
|
|
- uploadCount: blobUploads.length,
|
|
|
+ updatedAt: updatedAtTimestamp,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ event: "session_synced",
|
|
|
})
|
|
|
|
|
|
- // Emit session synced event with the latest updated_at timestamp
|
|
|
- const latestUpdatedAt = this.sessionUpdatedAt[sessionId]
|
|
|
- if (latestUpdatedAt) {
|
|
|
- const updatedAtTimestamp = new Date(latestUpdatedAt).getTime()
|
|
|
- this.onSessionSynced?.({
|
|
|
- sessionId,
|
|
|
- updatedAt: updatedAtTimestamp,
|
|
|
- timestamp: Date.now(),
|
|
|
- event: "session_synced",
|
|
|
- })
|
|
|
-
|
|
|
- this.logger?.debug("Emitted session_synced event", "SessionManager", {
|
|
|
- sessionId,
|
|
|
- updatedAt: updatedAtTimestamp,
|
|
|
- })
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- this.logger?.error("Failed to sync session", "SessionManager", {
|
|
|
- taskId,
|
|
|
- error: error instanceof Error ? error.message : String(error),
|
|
|
+ this.logger?.debug("Emitted session_synced event", "SessionManager", {
|
|
|
+ sessionId,
|
|
|
+ updatedAt: updatedAtTimestamp,
|
|
|
})
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.logger?.error("Failed to sync session", "SessionManager", {
|
|
|
+ taskId,
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
+ })
|
|
|
|
|
|
- const token = await this.getToken?.()
|
|
|
+ const token = await this.getToken?.()
|
|
|
|
|
|
- if (token) {
|
|
|
- this.tokenValid[token] = undefined
|
|
|
- }
|
|
|
+ if (token) {
|
|
|
+ this.tokenValid[token] = undefined
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (lastItem) {
|
|
|
- this.lastActiveSessionId = this.sessionPersistenceManager.getSessionForTask(lastItem.taskId) || null
|
|
|
+ if (lastItem) {
|
|
|
+ this.lastActiveSessionId = this.sessionPersistenceManager.getSessionForTask(lastItem.taskId) || null
|
|
|
|
|
|
- if (this.lastActiveSessionId) {
|
|
|
- this.sessionPersistenceManager.setLastSession(this.lastActiveSessionId)
|
|
|
- }
|
|
|
+ if (this.lastActiveSessionId) {
|
|
|
+ this.sessionPersistenceManager.setLastSession(this.lastActiveSessionId)
|
|
|
}
|
|
|
-
|
|
|
- this.logger?.debug("Session sync completed", "SessionManager", {
|
|
|
- lastSessionId: this.lastActiveSessionId,
|
|
|
- remainingQueueLength: this.queue.length,
|
|
|
- })
|
|
|
- } finally {
|
|
|
- this.isSyncing = false
|
|
|
}
|
|
|
+
|
|
|
+ this.logger?.debug("Session sync completed", "SessionManager", {
|
|
|
+ lastSessionId: this.lastActiveSessionId,
|
|
|
+ remainingQueueLength: this.queue.length,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * use this when exiting the process
|
|
|
- */
|
|
|
- destroy() {
|
|
|
- this.logger?.debug("Destroying SessionManager", "SessionManager")
|
|
|
+ async doSync(force = false) {
|
|
|
+ this.logger?.debug("Doing sync", "SessionManager")
|
|
|
+
|
|
|
+ if (this.pendingSync) {
|
|
|
+ this.logger?.debug("Found pending sync", "SessionManager")
|
|
|
|
|
|
- if (!this.pendingSync) {
|
|
|
- this.pendingSync = this.syncSession()
|
|
|
+ if (!force) {
|
|
|
+ this.logger?.debug("Not forced, returning pending sync", "SessionManager")
|
|
|
+
|
|
|
+ return this.pendingSync
|
|
|
+ } else {
|
|
|
+ this.logger?.debug("Forced, syncing despite pending sync", "SessionManager")
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ this.logger?.debug("Creating new sync", "SessionManager")
|
|
|
+
|
|
|
+ this.pendingSync = this.syncSession()
|
|
|
+
|
|
|
+ let pendingSync = this.pendingSync
|
|
|
+
|
|
|
+ void (async () => {
|
|
|
+ try {
|
|
|
+ await pendingSync
|
|
|
+ } finally {
|
|
|
+ if (this.pendingSync === pendingSync) {
|
|
|
+ this.pendingSync = null
|
|
|
+ }
|
|
|
+
|
|
|
+ this.logger?.debug("Nulling pending sync after resolution", "SessionManager")
|
|
|
+ }
|
|
|
+ })()
|
|
|
+
|
|
|
return this.pendingSync
|
|
|
}
|
|
|
|
|
|
@@ -1077,10 +1132,6 @@ Summary:`
|
|
|
|
|
|
cleanedSummary = cleanedSummary.replace(/^["']|["']$/g, "")
|
|
|
|
|
|
- if (cleanedSummary.length > 140) {
|
|
|
- cleanedSummary = cleanedSummary.substring(0, 137) + "..."
|
|
|
- }
|
|
|
-
|
|
|
if (cleanedSummary) {
|
|
|
return cleanedSummary
|
|
|
}
|