import { withSessionBindingLock } from './session-binding-lock.mjs'; import { initialSessionTitle } from './session-title.mjs'; export const WORKSPACE_SESSION_STALE = 'workspace-session-stale'; function workspaceSession(harness, sessionId) { if (typeof harness.workspaceSession === 'function') { return harness.workspaceSession(sessionId); } const session = { sessionId, sessionExists: (...args) => harness.sessionExists(sessionId, ...args), models: (...args) => harness.getSessionModels(sessionId, ...args), selectModel: (...args) => harness.selectSessionModel(sessionId, ...args), isRunning: (...args) => harness.isSessionRunning(sessionId, ...args), hasActiveTurn: (...args) => harness.hasActiveTurn(sessionId, ...args), stopActiveTurn: (...args) => harness.stopActiveTurn(sessionId, ...args), steerActiveTurn: (...args) => harness.steerActiveTurn(sessionId, ...args), ask: (...args) => harness.ask(sessionId, ...args), }; if (typeof harness.renameSession === 'function') { session.renameTitle = (...args) => harness.renameSession(sessionId, ...args); } return Object.freeze(session); } async function sessionExists(session, options) { return options === undefined ? session.sessionExists() : session.sessionExists(options); } async function createSession(harness, options) { return options === undefined ? harness.createSession() : harness.createSession(options); } /** * Resolve, persist, and ask through a session that belongs to the bot's * current workspace. A concurrent workspace switch invalidates the scoped * session and retries before any prompt is sent to the stale session. */ export async function askInWorkspaceSession({ harness, state, key, text, content, contextEnhanced = false, createOptions, existsOptions, askOptions, }) { const initialTitle = contextEnhanced ? initialSessionTitle({ text, content, files: typeof askOptions === 'object' ? askOptions?.files : undefined, }) : null; const renameSignal = createOptions?.signal ?? (typeof askOptions === 'object' ? askOptions?.signal : undefined); const renameOptions = renameSignal ? { signal: renameSignal } : undefined; while (true) { try { const binding = await withSessionBindingLock(state, key, async () => { let sessionId = state.sessionFor(key); let session = sessionId ? workspaceSession(harness, sessionId) : null; if (!session || !(await sessionExists(session, existsOptions))) { sessionId = await createSession(harness, createOptions); if (await state.setSession(key, sessionId) === false) return null; session = workspaceSession(harness, sessionId); if (initialTitle && typeof session.renameTitle === 'function') { try { await session.renameTitle(initialTitle, renameOptions); } catch (error) { if (error?.code === WORKSPACE_SESSION_STALE || renameOptions?.signal?.aborted) { throw error; } console.warn('[dsh-im] unable to set the initial Session title:', error?.message ?? error); } } } return { sessionId, session }; }); if (!binding) continue; const artifacts = []; const originalOnArtifact = typeof askOptions === 'object' && typeof askOptions?.onArtifact === 'function' ? askOptions.onArtifact : null; const artifactOptions = typeof askOptions === 'number' ? { timeoutMs: askOptions } : { ...askOptions }; artifactOptions.onArtifact = async (artifact) => { artifacts.push(artifact); await originalOnArtifact?.(artifact); }; const answer = await binding.session.ask(content ?? text, artifactOptions); return { sessionId: binding.sessionId, answer, ...(artifacts.length > 0 ? { artifacts } : {}), }; } catch (error) { if (error?.code !== WORKSPACE_SESSION_STALE) throw error; } } }