mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
Migrate worktree mappings to session metadata (#514)
## Summary - Upgrade `@opencode-ai/sdk` to `1.15.13` and adapt UI types for the current session/diff API surface. - Add a session metadata store helper that safely updates OpenCode session metadata via read-merge-full-replace while preserving non-CodeNomad metadata. - Migrate CodeNomad worktree assignments from `.codenomad/worktreeMap.json` into `metadata.codenomad.worktreeSlug`, with legacy fallback, stale-entry pruning, and server-side deletion of the legacy map once empty. - Add `.codenomad/background_processes/` to CodeNomad-managed `.git/info/exclude` entries. ## Details - New sessions and worktree reassignment now persist explicit session worktree assignments in OpenCode session metadata under: `metadata.codenomad.worktreeSlug` - Worktree resolution now prefers session metadata, then falls back to legacy `parentSessionWorktreeSlug`, then `root`. - Legacy migration runs after session/map loading; after the full session list is known, missing legacy session IDs are treated as stale and pruned. - The UI no longer uses `defaultWorktreeSlug`; the fallback is `root`. - `writeWorktreeMap` deletes `.codenomad/worktreeMap.json` server-side when no legacy parent-session mappings remain. ## Validation - `npm run typecheck --workspace @codenomad/ui` - `npm run typecheck --workspace @neuralnomads/codenomad`
This commit is contained in:
parent
b2855f4312
commit
873235ee1e
8 changed files with 265 additions and 53 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -3266,10 +3266,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz",
|
||||
"integrity": "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA==",
|
||||
"license": "MIT"
|
||||
"version": "1.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.13.tgz",
|
||||
"integrity": "sha512-4TwojIoQ8EG6/mVBuUVYZXiFcwNmiiytEnjnvyuvSJjGwFIlw2YIBFxtSVC3FbwwbwHT63teh1RHiQUUC4U5xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
|
|
@ -13500,7 +13503,7 @@
|
|||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "1.2.6",
|
||||
"@opencode-ai/sdk": "1.15.13",
|
||||
"@solidjs/router": "^0.13.0",
|
||||
"@suid/icons-material": "^0.9.0",
|
||||
"@suid/material": "^0.19.0",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ async function ensureGitExclude(repoRoot: string, logger?: LogLike): Promise<voi
|
|||
}
|
||||
|
||||
const entries = [
|
||||
".codenomad/background_processes/",
|
||||
".codenomad/worktrees/",
|
||||
".codenomad/worktreeMap.json",
|
||||
]
|
||||
|
|
@ -108,6 +109,11 @@ export async function writeWorktreeMap(workspaceFolder: string, next: WorktreeMa
|
|||
await ensureGitExclude(repoRoot, logger).catch(() => undefined)
|
||||
}
|
||||
|
||||
if (Object.keys(next.parentSessionWorktreeSlug ?? {}).length === 0) {
|
||||
await deleteWorktreeMap(workspaceFolder, logger)
|
||||
return
|
||||
}
|
||||
|
||||
const payload: WorktreeMap = {
|
||||
version: 1,
|
||||
defaultWorktreeSlug: next.defaultWorktreeSlug || "root",
|
||||
|
|
@ -120,6 +126,17 @@ export async function writeWorktreeMap(workspaceFolder: string, next: WorktreeMa
|
|||
await fsp.rename(tmpPath, filePath)
|
||||
}
|
||||
|
||||
export async function deleteWorktreeMap(workspaceFolder: string, logger?: LogLike): Promise<void> {
|
||||
const { repoRoot } = await resolveRepoRoot(workspaceFolder, logger)
|
||||
const filePath = getMapPath(repoRoot)
|
||||
try {
|
||||
await fsp.rm(filePath, { force: true })
|
||||
} catch (error) {
|
||||
logger?.warn?.({ err: error, filePath }, "Failed to delete worktree map")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function worktreeMapExists(repoRoot: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(getMapPath(repoRoot))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "1.2.6",
|
||||
"@opencode-ai/sdk": "1.15.13",
|
||||
"@solidjs/router": "^0.13.0",
|
||||
"@suid/icons-material": "^0.9.0",
|
||||
"@suid/material": "^0.19.0",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
type SessionStatus,
|
||||
} from "../types/session"
|
||||
import type { Message } from "../types/message"
|
||||
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
|
||||
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
import { instances } from "./instances"
|
||||
import { preferences, setAgentModelPreference } from "./preferences"
|
||||
|
|
@ -46,7 +46,9 @@ import {
|
|||
getOrCreateWorktreeClient,
|
||||
getRootClient,
|
||||
getWorktreeSlugForSession,
|
||||
removeParentSessionMapping,
|
||||
migrateLegacyWorktreeMapToSessionMetadata,
|
||||
pruneStaleLegacyWorktreeMapEntries,
|
||||
removeLegacyParentSessionMapping,
|
||||
setWorktreeSlugForParentSession,
|
||||
} from "./worktrees"
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ async function loadSessionDiff(instanceId: string, sessionId: string, force = fa
|
|||
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
||||
|
||||
try {
|
||||
const diffs = await requestData<FileDiff[]>(
|
||||
const diffs = await requestData<SnapshotFileDiff[]>(
|
||||
client.session.diff({ sessionID: sessionId }),
|
||||
"session.diff",
|
||||
)
|
||||
|
|
@ -105,6 +107,7 @@ interface SessionForkResponse {
|
|||
providerID?: string
|
||||
modelID?: string
|
||||
}
|
||||
metadata?: Record<string, unknown>
|
||||
time?: {
|
||||
created?: number
|
||||
updated?: number
|
||||
|
|
@ -189,6 +192,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||
time: {
|
||||
...apiSession.time,
|
||||
},
|
||||
metadata: apiSession.metadata ?? existingSession?.metadata,
|
||||
revert: apiSession.revert
|
||||
? {
|
||||
messageID: apiSession.revert.messageID,
|
||||
|
|
@ -233,6 +237,12 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||
.map((session) => session.id)
|
||||
|
||||
await Promise.all(parentIds.map((parentId) => fetchSessionChildren(instanceId, parentId)))
|
||||
void (async () => {
|
||||
await migrateLegacyWorktreeMapToSessionMetadata(instanceId)
|
||||
await pruneStaleLegacyWorktreeMapEntries(instanceId)
|
||||
})().catch((error) => {
|
||||
log.warn("Failed to finish legacy worktree map migration", { instanceId, error })
|
||||
})
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch sessions:", error)
|
||||
throw error
|
||||
|
|
@ -260,6 +270,7 @@ function toClientSession(instanceId: string, apiSession: any, existingSession?:
|
|||
time: {
|
||||
...apiSession.time,
|
||||
},
|
||||
metadata: apiSession.metadata ?? existingSession?.metadata,
|
||||
revert: apiSession.revert
|
||||
? {
|
||||
messageID: apiSession.revert.messageID,
|
||||
|
|
@ -384,6 +395,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||
time: {
|
||||
...response.data.time,
|
||||
},
|
||||
metadata: response.data.metadata,
|
||||
revert: response.data.revert
|
||||
? {
|
||||
messageID: response.data.revert.messageID,
|
||||
|
|
@ -439,7 +451,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||
}
|
||||
|
||||
// Persist mapping for this *parent* session (best-effort).
|
||||
await setWorktreeSlugForParentSession(instanceId, session.id, worktreeSlug).catch((error) => {
|
||||
await setWorktreeSlugForParentSession(instanceId, session.id, worktreeSlug, { currentSlug: worktreeSlug }).catch((error) => {
|
||||
log.warn("Failed to persist session worktree mapping", { instanceId, sessionId: session.id, worktreeSlug, error })
|
||||
})
|
||||
|
||||
|
|
@ -485,13 +497,14 @@ async function forkSession(
|
|||
title: info.title || "Forked Session",
|
||||
parentId: info.parentID || null,
|
||||
agent: info.agent || "",
|
||||
model: {
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
},
|
||||
status: "idle",
|
||||
idleSince: null,
|
||||
version: "0",
|
||||
model: {
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
},
|
||||
status: "idle",
|
||||
idleSince: null,
|
||||
version: "0",
|
||||
metadata: info.metadata,
|
||||
time: info.time ? { ...info.time } : { created: Date.now(), updated: Date.now() },
|
||||
revert: info.revert
|
||||
? {
|
||||
|
|
@ -612,7 +625,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
|||
|
||||
// Clean up mapping for deleted parent sessions.
|
||||
if (deletingSession?.parentId === null) {
|
||||
await removeParentSessionMapping(instanceId, sessionId).catch(() => undefined)
|
||||
await removeLegacyParentSessionMapping(instanceId, sessionId).catch(() => undefined)
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to delete session:", error)
|
||||
|
|
|
|||
|
|
@ -478,6 +478,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||
retry: null,
|
||||
idleSince: null,
|
||||
version: info.version || "0",
|
||||
metadata: (info as any).metadata,
|
||||
time: info.time
|
||||
? { ...info.time }
|
||||
: {
|
||||
|
|
@ -523,6 +524,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||
parentId: info.parentID ?? existingSession.parentId,
|
||||
status: existingSession.status ?? "idle",
|
||||
retry: existingSession.retry ?? null,
|
||||
metadata: (info as any).metadata ?? existingSession.metadata,
|
||||
time: mergedTime,
|
||||
revert: info.revert
|
||||
? {
|
||||
|
|
|
|||
107
packages/ui/src/stores/session-metadata.ts
Normal file
107
packages/ui/src/stores/session-metadata.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { requestData } from "../lib/opencode-api"
|
||||
import { sessions, withSession } from "./session-state"
|
||||
|
||||
const CODENOMAD_METADATA_KEY = "codenomad"
|
||||
const CODENOMAD_METADATA_VERSION = 1
|
||||
|
||||
export interface CodeNomadSessionMetadata {
|
||||
version: 1
|
||||
worktreeSlug?: string
|
||||
}
|
||||
|
||||
type MetadataRecord = Record<string, unknown>
|
||||
|
||||
function isRecord(value: unknown): value is MetadataRecord {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function normalizeMetadata(value: unknown): MetadataRecord {
|
||||
return isRecord(value) ? { ...value } : {}
|
||||
}
|
||||
|
||||
function normalizeCodeNomadMetadata(value: unknown): CodeNomadSessionMetadata {
|
||||
const source = isRecord(value) ? value : {}
|
||||
const metadata: CodeNomadSessionMetadata = { version: CODENOMAD_METADATA_VERSION }
|
||||
if (typeof source.worktreeSlug === "string" && source.worktreeSlug.trim()) {
|
||||
metadata.worktreeSlug = source.worktreeSlug
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
function mergeCodeNomadMetadata(
|
||||
metadata: MetadataRecord,
|
||||
updater: (current: CodeNomadSessionMetadata) => CodeNomadSessionMetadata,
|
||||
): MetadataRecord {
|
||||
const currentCodeNomad = isRecord(metadata[CODENOMAD_METADATA_KEY])
|
||||
? { ...(metadata[CODENOMAD_METADATA_KEY] as MetadataRecord) }
|
||||
: {}
|
||||
const nextCodeNomad = updater(normalizeCodeNomadMetadata(currentCodeNomad))
|
||||
const mergedCodeNomad: MetadataRecord = {
|
||||
...currentCodeNomad,
|
||||
...nextCodeNomad,
|
||||
version: CODENOMAD_METADATA_VERSION,
|
||||
}
|
||||
|
||||
if (!nextCodeNomad.worktreeSlug) {
|
||||
delete mergedCodeNomad.worktreeSlug
|
||||
}
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
[CODENOMAD_METADATA_KEY]: mergedCodeNomad,
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionMetadata(instanceId: string, sessionId: string): MetadataRecord {
|
||||
return normalizeMetadata(sessions().get(instanceId)?.get(sessionId)?.metadata)
|
||||
}
|
||||
|
||||
export function getCodeNomadSessionMetadata(instanceId: string, sessionId: string): CodeNomadSessionMetadata {
|
||||
return normalizeCodeNomadMetadata(getSessionMetadata(instanceId, sessionId)[CODENOMAD_METADATA_KEY])
|
||||
}
|
||||
|
||||
export async function updateSessionMetadataWithClient(
|
||||
client: OpencodeClient,
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
updater: (metadata: MetadataRecord) => MetadataRecord,
|
||||
): Promise<MetadataRecord> {
|
||||
const latest = await requestData<any>(client.session.get({ sessionID: sessionId }), "session.get")
|
||||
const nextMetadata = updater(normalizeMetadata(latest?.metadata))
|
||||
const updated = await requestData<any>(
|
||||
client.session.update({ sessionID: sessionId, metadata: nextMetadata }),
|
||||
"session.update",
|
||||
)
|
||||
const persistedMetadata = normalizeMetadata(updated?.metadata ?? nextMetadata)
|
||||
|
||||
withSession(instanceId, sessionId, (session) => {
|
||||
session.metadata = persistedMetadata
|
||||
})
|
||||
|
||||
return persistedMetadata
|
||||
}
|
||||
|
||||
export async function updateCodeNomadSessionMetadataWithClient(
|
||||
client: OpencodeClient,
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
updater: (metadata: CodeNomadSessionMetadata) => CodeNomadSessionMetadata,
|
||||
): Promise<CodeNomadSessionMetadata> {
|
||||
const persisted = await updateSessionMetadataWithClient(client, instanceId, sessionId, (metadata) =>
|
||||
mergeCodeNomadMetadata(metadata, updater),
|
||||
)
|
||||
return normalizeCodeNomadMetadata(persisted[CODENOMAD_METADATA_KEY])
|
||||
}
|
||||
|
||||
export async function setSessionWorktreeSlugWithClient(
|
||||
client: OpencodeClient,
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
worktreeSlug: string,
|
||||
): Promise<void> {
|
||||
await updateCodeNomadSessionMetadataWithClient(client, instanceId, sessionId, (metadata) => ({
|
||||
...metadata,
|
||||
worktreeSlug,
|
||||
}))
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { serverApi } from "../lib/api-client"
|
|||
import { sdkManager, type OpencodeClient } from "../lib/sdk-manager"
|
||||
import { sessions } from "./session-state"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import { getCodeNomadSessionMetadata, setSessionWorktreeSlugWithClient } from "./session-metadata"
|
||||
|
||||
const log = getLogger("api")
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ const [gitRepoStatusByInstance, setGitRepoStatusByInstance] = createSignal<Map<s
|
|||
|
||||
const worktreeLoads = new Map<string, Promise<void>>()
|
||||
const mapLoads = new Map<string, Promise<void>>()
|
||||
const mapMigrations = new Map<string, Promise<void>>()
|
||||
|
||||
function normalizeMap(input?: WorktreeMap | null): WorktreeMap {
|
||||
if (!input || typeof input !== "object") {
|
||||
|
|
@ -124,9 +126,28 @@ async function deleteWorktree(instanceId: string, slug: string, options?: { forc
|
|||
if (!trimmed || trimmed === "root") {
|
||||
throw new Error("Invalid worktree")
|
||||
}
|
||||
await moveSessionsFromDeletedWorktree(instanceId, trimmed).catch((error) => {
|
||||
log.warn("Failed to move sessions from deleted worktree", { instanceId, slug: trimmed, error })
|
||||
})
|
||||
await serverApi.deleteWorktree(instanceId, trimmed, options)
|
||||
}
|
||||
|
||||
async function moveSessionsFromDeletedWorktree(instanceId: string, slug: string): Promise<void> {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
if (!instanceSessions) return
|
||||
|
||||
const parentSessionIds = Array.from(instanceSessions.values())
|
||||
.filter((session) => !session.parentId)
|
||||
.filter((session) => getWorktreeSlugForParentSession(instanceId, session.id) === slug)
|
||||
.map((session) => session.id)
|
||||
|
||||
for (const parentSessionId of parentSessionIds) {
|
||||
const client = getOrCreateWorktreeClient(instanceId, slug)
|
||||
await setSessionWorktreeSlugWithClient(client, instanceId, parentSessionId, "root")
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWorktreeMapLoaded(instanceId: string): Promise<void> {
|
||||
if (!instanceId) return
|
||||
if (worktreeMapByInstance().has(instanceId)) return
|
||||
|
|
@ -146,6 +167,9 @@ async function ensureWorktreeMapLoaded(instanceId: string): Promise<void> {
|
|||
if (worktreesByInstance().has(instanceId)) {
|
||||
void pruneWorktreeMap(instanceId).catch(() => undefined)
|
||||
}
|
||||
void migrateLegacyWorktreeMapToSessionMetadata(instanceId).catch((error) => {
|
||||
log.warn("Failed to migrate legacy worktree map", { instanceId, error })
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn("Failed to load worktree map", { instanceId, error })
|
||||
|
|
@ -245,23 +269,7 @@ async function pruneWorktreeMap(instanceId: string): Promise<boolean> {
|
|||
}
|
||||
|
||||
function getDefaultWorktreeSlug(instanceId: string): string {
|
||||
return normalizeWorktreeSlug(instanceId, getWorktreeMap(instanceId).defaultWorktreeSlug || "root")
|
||||
}
|
||||
|
||||
async function setDefaultWorktreeSlug(instanceId: string, slug: string): Promise<void> {
|
||||
await ensureWorktreeMapLoaded(instanceId)
|
||||
const current = getWorktreeMap(instanceId)
|
||||
const nextSlug = normalizeWorktreeSlug(instanceId, slug)
|
||||
const next: WorktreeMap = { ...current, defaultWorktreeSlug: nextSlug }
|
||||
setWorktreeMapByInstance((prev) => {
|
||||
const map = new Map(prev)
|
||||
map.set(instanceId, next)
|
||||
return map
|
||||
})
|
||||
|
||||
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
|
||||
log.warn("Failed to persist default worktree", { instanceId, slug: nextSlug, error })
|
||||
})
|
||||
return normalizeWorktreeSlug(instanceId, "root")
|
||||
}
|
||||
|
||||
function getParentSessionId(instanceId: string, sessionId: string): string {
|
||||
|
|
@ -271,8 +279,13 @@ function getParentSessionId(instanceId: string, sessionId: string): string {
|
|||
}
|
||||
|
||||
function getWorktreeSlugForParentSession(instanceId: string, parentSessionId: string): string {
|
||||
const metadataSlug = getCodeNomadSessionMetadata(instanceId, parentSessionId).worktreeSlug
|
||||
if (metadataSlug) {
|
||||
return normalizeWorktreeSlug(instanceId, metadataSlug)
|
||||
}
|
||||
|
||||
const map = getWorktreeMap(instanceId)
|
||||
const candidate = map.parentSessionWorktreeSlug[parentSessionId] ?? map.defaultWorktreeSlug ?? "root"
|
||||
const candidate = map.parentSessionWorktreeSlug[parentSessionId] ?? "root"
|
||||
return normalizeWorktreeSlug(instanceId, candidate)
|
||||
}
|
||||
|
||||
|
|
@ -281,27 +294,33 @@ function getWorktreeSlugForSession(instanceId: string, sessionId: string): strin
|
|||
return getWorktreeSlugForParentSession(instanceId, parentId)
|
||||
}
|
||||
|
||||
async function setWorktreeSlugForParentSession(instanceId: string, parentSessionId: string, slug: string): Promise<void> {
|
||||
async function setWorktreeSlugForParentSession(
|
||||
instanceId: string,
|
||||
parentSessionId: string,
|
||||
slug: string,
|
||||
options: { currentSlug?: string } = {},
|
||||
): Promise<void> {
|
||||
await ensureWorktreeMapLoaded(instanceId)
|
||||
const current = getWorktreeMap(instanceId)
|
||||
const normalizedSlug = normalizeWorktreeSlug(instanceId, slug)
|
||||
const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) }
|
||||
nextMapping[parentSessionId] = normalizedSlug
|
||||
const next: WorktreeMap = { ...current, parentSessionWorktreeSlug: nextMapping }
|
||||
setWorktreeMapByInstance((prev) => {
|
||||
const map = new Map(prev)
|
||||
map.set(instanceId, next)
|
||||
return map
|
||||
})
|
||||
|
||||
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
|
||||
log.warn("Failed to persist session worktree mapping", { instanceId, parentSessionId, slug: normalizedSlug, error })
|
||||
})
|
||||
const currentSlug = options.currentSlug
|
||||
? normalizeWorktreeSlug(instanceId, options.currentSlug)
|
||||
: getWorktreeSlugForParentSession(instanceId, parentSessionId)
|
||||
const client = getOrCreateWorktreeClient(instanceId, currentSlug)
|
||||
await setSessionWorktreeSlugWithClient(client, instanceId, parentSessionId, normalizedSlug)
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId, current)
|
||||
}
|
||||
|
||||
async function removeParentSessionMapping(instanceId: string, parentSessionId: string): Promise<void> {
|
||||
await ensureWorktreeMapLoaded(instanceId)
|
||||
const current = getWorktreeMap(instanceId)
|
||||
const currentSlug = getWorktreeSlugForParentSession(instanceId, parentSessionId)
|
||||
const client = getOrCreateWorktreeClient(instanceId, currentSlug)
|
||||
await setSessionWorktreeSlugWithClient(client, instanceId, parentSessionId, "root")
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId, current)
|
||||
}
|
||||
|
||||
async function removeLegacyParentSessionMapping(instanceId: string, parentSessionId: string, current = getWorktreeMap(instanceId)): Promise<void> {
|
||||
if (!current.parentSessionWorktreeSlug[parentSessionId]) return
|
||||
const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) }
|
||||
delete nextMapping[parentSessionId]
|
||||
|
|
@ -313,10 +332,58 @@ async function removeParentSessionMapping(instanceId: string, parentSessionId: s
|
|||
})
|
||||
|
||||
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
|
||||
log.warn("Failed to persist session worktree mapping removal", { instanceId, parentSessionId, error })
|
||||
log.warn("Failed to persist legacy worktree mapping removal", { instanceId, parentSessionId, error })
|
||||
})
|
||||
}
|
||||
|
||||
async function migrateLegacyWorktreeMapToSessionMetadata(instanceId: string): Promise<void> {
|
||||
if (!instanceId) return
|
||||
const existing = mapMigrations.get(instanceId)
|
||||
if (existing) return existing
|
||||
|
||||
const task = (async () => {
|
||||
const map = getWorktreeMap(instanceId)
|
||||
const entries = Object.entries(map.parentSessionWorktreeSlug ?? {})
|
||||
if (entries.length === 0) return
|
||||
|
||||
for (const [parentSessionId, legacySlug] of entries) {
|
||||
const parentSession = sessions().get(instanceId)?.get(parentSessionId)
|
||||
if (!parentSession) continue
|
||||
if (getCodeNomadSessionMetadata(instanceId, parentSessionId).worktreeSlug) {
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId)
|
||||
continue
|
||||
}
|
||||
|
||||
const normalizedSlug = normalizeWorktreeSlug(instanceId, legacySlug || "root")
|
||||
const client = getOrCreateWorktreeClient(instanceId, normalizedSlug)
|
||||
await setSessionWorktreeSlugWithClient(client, instanceId, parentSessionId, normalizedSlug)
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId)
|
||||
}
|
||||
})().finally(() => {
|
||||
mapMigrations.delete(instanceId)
|
||||
})
|
||||
|
||||
mapMigrations.set(instanceId, task)
|
||||
return task
|
||||
}
|
||||
|
||||
async function pruneStaleLegacyWorktreeMapEntries(instanceId: string): Promise<void> {
|
||||
if (!instanceId) return
|
||||
const migration = mapMigrations.get(instanceId)
|
||||
if (migration) await migration.catch(() => undefined)
|
||||
|
||||
const map = getWorktreeMap(instanceId)
|
||||
const entries = Object.entries(map.parentSessionWorktreeSlug ?? {})
|
||||
if (entries.length === 0) return
|
||||
|
||||
const instanceSessions = sessions().get(instanceId) ?? new Map()
|
||||
for (const [parentSessionId] of entries) {
|
||||
if (!instanceSessions.has(parentSessionId)) {
|
||||
await removeLegacyParentSessionMapping(instanceId, parentSessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWorktreeSlugForDirectory(instanceId: string, directory: string | undefined): string | null {
|
||||
if (!directory) return null
|
||||
const list = getWorktrees(instanceId)
|
||||
|
|
@ -377,12 +444,14 @@ export {
|
|||
getWorktrees,
|
||||
getWorktreeMap,
|
||||
getDefaultWorktreeSlug,
|
||||
setDefaultWorktreeSlug,
|
||||
getParentSessionId,
|
||||
getWorktreeSlugForParentSession,
|
||||
getWorktreeSlugForSession,
|
||||
setWorktreeSlugForParentSession,
|
||||
removeParentSessionMapping,
|
||||
migrateLegacyWorktreeMapToSessionMetadata,
|
||||
pruneStaleLegacyWorktreeMapEntries,
|
||||
removeLegacyParentSessionMapping,
|
||||
getWorktreeSlugForDirectory,
|
||||
buildWorktreeProxyPath,
|
||||
buildWorktreeProxyPathWithDirectoryOverride,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type {
|
|||
Provider as SDKProvider,
|
||||
Model as SDKModel,
|
||||
} from "@opencode-ai/sdk"
|
||||
import type { SessionStatus as SDKSessionStatus, FileDiff } from "@opencode-ai/sdk/v2/client"
|
||||
import type { SessionStatus as SDKSessionStatus, SnapshotFileDiff } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
// Export SDK types for external use
|
||||
export type {
|
||||
|
|
@ -76,7 +76,8 @@ export interface Session
|
|||
status: SessionStatus // Single source of truth for session status
|
||||
retry?: SessionRetryState | null // Retry metadata for transient backoff states
|
||||
idleSince?: number | null // Timestamp set when work finished but the session has not been viewed yet
|
||||
diff?: FileDiff[] // Session-level file diffs (hydrated via session.diff)
|
||||
metadata?: Record<string, unknown> // Session metadata persisted by OpenCode
|
||||
diff?: SnapshotFileDiff[] // Session-level file diffs (hydrated via session.diff)
|
||||
}
|
||||
|
||||
// Adapter function to convert SDK Session to client Session
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue