dsh-im-ops/src/channels/shared/workspace-session.mjs

110 lines
4 KiB
JavaScript

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;
}
}
}