Files
memind/session-reconcile.mjs
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

168 lines
4.6 KiB
JavaScript

import path from 'node:path';
import { developerToolsFromPolicy } from './capabilities.mjs';
import { buildSandboxSessionConstraints } from './user-publish.mjs';
function extensionName(config) {
return config?.name ?? null;
}
export function allowedExtensionNames(extensionOverrides) {
if (!extensionOverrides) return null;
return new Set(extensionOverrides.map((item) => item.name).filter(Boolean));
}
export function extensionToolsKey(config) {
const tools = config?.available_tools ?? config?.availableTools ?? [];
return [...tools].sort().join('\0');
}
export function extensionConfigsMatch(sessionExt, desiredConfig) {
if (extensionName(sessionExt) !== extensionName(desiredConfig)) return false;
return extensionToolsKey(sessionExt) === extensionToolsKey(desiredConfig);
}
export function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
const current = currentExtensions ?? [];
const desired = desiredExtensions ?? [];
const toRemove = [];
const toAdd = [];
for (const ext of current) {
const name = extensionName(ext);
if (!name) continue;
const wanted = desired.find((item) => extensionName(item) === name);
if (!wanted) {
toRemove.push(name);
continue;
}
if (!extensionConfigsMatch(ext, wanted)) {
toRemove.push(name);
toAdd.push(wanted);
}
}
for (const config of desired) {
const name = extensionName(config);
if (!name) continue;
const exists = current.some((ext) => extensionName(ext) === name);
if (!exists) {
toAdd.push(config);
}
}
return { toRemove, toAdd };
}
function samePath(left, right) {
if (!left || !right) return false;
return path.resolve(left) === path.resolve(right);
}
async function readJson(upstream) {
const text = await upstream.text();
if (!upstream.ok) {
throw new Error(text || `upstream ${upstream.status}`);
}
return text ? JSON.parse(text) : null;
}
/**
* Re-apply H5 session policy after resume so stale extension sets cannot bypass capability limits.
*/
export async function reconcileAgentSession(
apiFetch,
sessionId,
{ workingDir, sessionPolicy, sandboxConstraints = null },
) {
if (sessionPolicy?.unrestricted) return;
const session = await readJson(await apiFetch(`/sessions/${sessionId}`));
if (!samePath(session?.working_dir, workingDir)) {
await readJson(
await apiFetch('/agent/update_working_dir', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, working_dir: workingDir }),
}),
);
}
if (sessionPolicy?.gooseMode) {
await readJson(
await apiFetch('/agent/update_session', {
method: 'POST',
body: JSON.stringify({
session_id: sessionId,
goose_mode: sessionPolicy.gooseMode,
}),
}),
);
}
const desired = sessionPolicy?.extensionOverrides ?? [];
const allowed = allowedExtensionNames(desired);
const currentPayload = await readJson(await apiFetch(`/sessions/${sessionId}/extensions`));
const current = currentPayload?.extensions ?? [];
for (const ext of current) {
const name = extensionName(ext);
if (!name || allowed.has(name)) continue;
await readJson(
await apiFetch('/agent/remove_extension', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, name }),
}),
);
}
const refreshedPayload = await readJson(await apiFetch(`/sessions/${sessionId}/extensions`));
const refreshed = refreshedPayload?.extensions ?? [];
const { toRemove, toAdd } = extensionsNeedingRefresh(refreshed, desired);
for (const name of toRemove) {
await readJson(
await apiFetch('/agent/remove_extension', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, name }),
}),
);
}
for (const config of toAdd) {
await readJson(
await apiFetch('/agent/add_extension', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, config }),
}),
);
}
await readJson(
await apiFetch('/agent/restart', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId }),
}),
);
if (sandboxConstraints?.trim()) {
const sandboxText = buildSandboxSessionConstraints({
baseConstraints: sandboxConstraints,
developerTools: developerToolsFromPolicy(sessionPolicy),
});
await apiFetch('/agent/harness_remember', {
method: 'POST',
body: JSON.stringify({
sessionId,
content: sandboxText,
title: 'TKMind 用户空间沙箱',
}),
});
await apiFetch('/agent/harness_bootstrap', {
method: 'POST',
body: JSON.stringify({
sessionId,
force: true,
}),
});
}
}