Files
memind/session-reconcile.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

182 lines
5.0 KiB
JavaScript

import path from 'node:path';
import { developerToolsFromPolicy } from './capabilities.mjs';
import { buildSessionMemoryEntries } from './user-memory-profile.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, userContext = 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 }),
}),
);
const sandboxText = sandboxConstraints?.trim()
? buildSandboxSessionConstraints({
baseConstraints: sandboxConstraints,
developerTools: developerToolsFromPolicy(sessionPolicy),
})
: null;
const memoryEntries = buildSessionMemoryEntries({
workingDir,
sessionPolicy,
sandboxConstraints: sandboxText,
userContext,
});
if (memoryEntries.length > 0) {
for (const entry of memoryEntries) {
if (!entry.content?.trim()) continue;
await apiFetch('/agent/harness_remember', {
method: 'POST',
body: JSON.stringify({
sessionId,
content: entry.content,
title: entry.title,
}),
});
}
await apiFetch('/agent/harness_bootstrap', {
method: 'POST',
body: JSON.stringify({
sessionId,
force: true,
}),
});
}
}