mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-20 20:43:21 +08:00
feat: bind bot chats to existing harness sessions
This commit is contained in:
parent
76d5430f1f
commit
1af8610cd5
21 changed files with 1807 additions and 130 deletions
110
src/channels/shared/harness-session-binding.mjs
Normal file
110
src/channels/shared/harness-session-binding.mjs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { isAbsolute } from 'node:path';
|
||||
|
||||
const MAX_SESSION_ID_LENGTH = 256;
|
||||
const UNSAFE_SESSION_ID = /[\p{White_Space}\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
||||
const UNSAFE_WORKSPACE_PATH = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
||||
|
||||
function bindingError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function validatedSessionId(value) {
|
||||
if (typeof value !== 'string' || !value || value.length > MAX_SESSION_ID_LENGTH
|
||||
|| UNSAFE_SESSION_ID.test(value)) {
|
||||
throw bindingError('session-id-invalid', 'A non-empty, safe session id is required');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sessionWorkspace(sessionId, value) {
|
||||
if (!Array.isArray(value?.items) || !Array.isArray(value?.archivedSessionIds)
|
||||
|| value.archivedSessionIds.some((id) => typeof id !== 'string' || !id)) {
|
||||
throw new Error('Harness returned an invalid response for workspace.list');
|
||||
}
|
||||
|
||||
const owners = [];
|
||||
for (const workspace of value.items) {
|
||||
if (typeof workspace?.workspaceId !== 'string' || !workspace.workspaceId
|
||||
|| typeof workspace.path !== 'string' || !isAbsolute(workspace.path)
|
||||
|| !Array.isArray(workspace.sessionIds)
|
||||
|| workspace.sessionIds.some((id) => typeof id !== 'string' || !id)) {
|
||||
throw new Error('Harness returned an invalid response for workspace.list');
|
||||
}
|
||||
for (const accountedId of workspace.sessionIds) {
|
||||
if (accountedId === sessionId) owners.push(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
if (owners.length === 0) {
|
||||
throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
|
||||
}
|
||||
if (owners.length !== 1) {
|
||||
throw bindingError(
|
||||
'session-workspace-ambiguous',
|
||||
'The session is registered to more than one Harness workspace',
|
||||
);
|
||||
}
|
||||
if (UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
|
||||
throw new Error('Harness returned an unsafe workspace path for the session');
|
||||
}
|
||||
return {
|
||||
workspace: owners[0],
|
||||
archived: value.archivedSessionIds.includes(sessionId),
|
||||
};
|
||||
}
|
||||
|
||||
function sessionSummary(sessionId, value) {
|
||||
if (!Array.isArray(value?.items)
|
||||
|| value.items.some((item) => typeof item?.sessionId !== 'string' || !item.sessionId)) {
|
||||
throw new Error('Harness returned an invalid response for session.list');
|
||||
}
|
||||
const matches = value.items.filter((item) => item.sessionId === sessionId);
|
||||
if (matches.length === 0) {
|
||||
throw bindingError('session-summary-unavailable', 'The session is no longer available from Harness');
|
||||
}
|
||||
if (matches.length !== 1) {
|
||||
throw new Error('Harness returned duplicate session summaries for session.list');
|
||||
}
|
||||
|
||||
const [summary] = matches;
|
||||
if (summary.origin === 'subagent') {
|
||||
throw bindingError(
|
||||
'session-subagent-unsupported',
|
||||
'Subagent sessions cannot be adopted as a bot conversation',
|
||||
);
|
||||
}
|
||||
if (summary.origin !== undefined) {
|
||||
throw new Error('Harness returned an invalid session origin for session.list');
|
||||
}
|
||||
const title = summary.projections?.values?.title;
|
||||
if (title !== undefined && title !== null && typeof title !== 'string') {
|
||||
throw new Error('Harness returned an invalid session title for session.list');
|
||||
}
|
||||
return { title: typeof title === 'string' ? title : null };
|
||||
}
|
||||
|
||||
export async function adoptRegisteredWorkspaceSession(client, value, options = {}, timeoutMs = 30_000) {
|
||||
const sessionId = validatedSessionId(value);
|
||||
await client.ensureRunning(options);
|
||||
const workspaceList = await client.rpc('workspace.list', {}, timeoutMs, options);
|
||||
const { workspace, archived } = sessionWorkspace(sessionId, workspaceList);
|
||||
const summary = sessionSummary(
|
||||
sessionId,
|
||||
await client.rpc('session.list', {}, timeoutMs, options),
|
||||
);
|
||||
const adopted = await client.rpc('session.create', {
|
||||
workspaceId: workspace.workspaceId,
|
||||
sessionId,
|
||||
}, timeoutMs, options);
|
||||
if (!adopted || adopted.sessionId !== sessionId) {
|
||||
throw new Error('Harness returned an invalid response for session.create');
|
||||
}
|
||||
return {
|
||||
sessionId,
|
||||
workspace: workspace.path,
|
||||
title: summary.title,
|
||||
archived,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue