fix(ui): scope agent memory to project context owner
This commit is contained in:
@@ -6,46 +6,26 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { formatDirectoryName } from '@/lib/utils';
|
import { formatDirectoryName } from '@/lib/utils';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory, isChatDirectoryPath } from '@/lib/chatDirectories';
|
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { useProjectContextOwner } from '@/hooks/useProjectContextOwner';
|
||||||
|
|
||||||
export const ProjectContextPanel: React.FC<{
|
export const ProjectContextPanel: React.FC<{
|
||||||
onActionComplete?: () => void;
|
onActionComplete?: () => void;
|
||||||
onOpenPlan?: (plan: { id: string; title: string }) => void;
|
onOpenPlan?: (plan: { id: string; title: string }) => void;
|
||||||
}> = ({ onActionComplete, onOpenPlan }) => {
|
}> = ({ onActionComplete, onOpenPlan }) => {
|
||||||
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
|
||||||
const projects = useProjectsStore((state) => state.projects);
|
const projects = useProjectsStore((state) => state.projects);
|
||||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const gitDirectories = useGitStore((state) => state.directories);
|
const gitDirectories = useGitStore((state) => state.directories);
|
||||||
const isChatContext = useSessionUIStore((state) => (
|
|
||||||
state.newSessionDraft.open
|
|
||||||
? state.newSessionDraft.target === 'chat'
|
|
||||||
: isChatDirectoryPath(state.currentSessionDirectory)
|
|
||||||
));
|
|
||||||
const chatSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
|
const chatSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
|
||||||
const chatsRoot = getChatsRootFromDirectory(chatSessionDirectory) ?? getChatsRootForHome(homeDirectory);
|
const projectRef = useProjectContextOwner(chatSessionDirectory);
|
||||||
|
const isChatContext = projectRef?.id === CHAT_DRAFT_PROJECT_ID;
|
||||||
|
|
||||||
const activeProject = React.useMemo(() => {
|
const activeProject = React.useMemo(() => {
|
||||||
if (isChatContext) return null;
|
if (isChatContext) return null;
|
||||||
if (activeProjectId) {
|
return projects.find((project) => project.id === projectRef?.id) ?? null;
|
||||||
return projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
|
}, [isChatContext, projectRef?.id, projects]);
|
||||||
}
|
|
||||||
return projects[0] ?? null;
|
|
||||||
}, [activeProjectId, isChatContext, projects]);
|
|
||||||
|
|
||||||
const projectRef = React.useMemo(() => {
|
|
||||||
if (isChatContext && chatsRoot) {
|
|
||||||
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
|
|
||||||
}
|
|
||||||
if (!activeProject) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: activeProject.id,
|
|
||||||
path: activeProject.path,
|
|
||||||
};
|
|
||||||
}, [activeProject, chatsRoot, isChatContext]);
|
|
||||||
|
|
||||||
const projectLabel = React.useMemo(() => {
|
const projectLabel = React.useMemo(() => {
|
||||||
if (isChatContext) return t('sessions.sidebar.activity.chatsTitle');
|
if (isChatContext) return t('sessions.sidebar.activity.chatsTitle');
|
||||||
|
|||||||
@@ -106,10 +106,16 @@ its own tool. It feeds this panel only — what a session is told about memory i
|
|||||||
decided server-side by `packages/web/server/lib/session-knowledge`, so it
|
decided server-side by `packages/web/server/lib/session-knowledge`, so it
|
||||||
reaches sessions that have no UI at all and survives compaction.
|
reaches sessions that have no UI at all and survives compaction.
|
||||||
|
|
||||||
Both sides resolve a worktree to its project before touching the store — the
|
`useProjectContextOwner` is the client authority shared by this panel and the
|
||||||
client through `resolveProjectForSessionDirectory`, the server through
|
memory sync. It resolves managed chat directories to the Chats root and a
|
||||||
`agent-memory/project-resolution`. Keying by the session directory instead filed
|
worktree to its project before either consumer touches a store. The server uses
|
||||||
a worktree's memories under a project nothing reads.
|
`agent-memory/project-resolution` for the same worktree rule. Keying by a
|
||||||
|
worktree session directory would file memories under a project nothing reads.
|
||||||
|
|
||||||
|
Project memory is rendered only when the store's `projectPath` matches the
|
||||||
|
panel owner. An owner switch hides the previous project's entries before the
|
||||||
|
new request starts. A failed request marks the new owner unavailable instead of
|
||||||
|
presenting that hidden list as authoritative empty memory.
|
||||||
|
|
||||||
Turning the switch back on re-reads the store only after the setting has
|
Turning the switch back on re-reads the store only after the setting has
|
||||||
finished being written. The switch flips the client immediately, which makes the
|
finished being written. The switch flips the client immediately, which makes the
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useI18n } from '@/lib/i18n';
|
|||||||
import { AGENT_MEMORY_BODY_MAX_LENGTH, AGENT_MEMORY_TITLE_MAX_LENGTH, type AgentMemoryEntry, type AgentMemoryScope } from '@/lib/agentMemoryApi';
|
import { AGENT_MEMORY_BODY_MAX_LENGTH, AGENT_MEMORY_TITLE_MAX_LENGTH, type AgentMemoryEntry, type AgentMemoryScope } from '@/lib/agentMemoryApi';
|
||||||
import { classifyMemory, memoryViewKey, type MemoryBadge } from '@/lib/agentMemoryBadges';
|
import { classifyMemory, memoryViewKey, type MemoryBadge } from '@/lib/agentMemoryBadges';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
import { selectProjectMemoryForPath, useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,7 +160,7 @@ export const MemorySection: React.FC<{
|
|||||||
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const globalEntries = useAgentMemoryStore((state) => state.global);
|
const globalEntries = useAgentMemoryStore((state) => state.global);
|
||||||
const projectEntries = useAgentMemoryStore((state) => state.project);
|
const projectEntries = useAgentMemoryStore((state) => selectProjectMemoryForPath(state, projectPath));
|
||||||
const globalFailed = useAgentMemoryStore((state) => state.globalFailed);
|
const globalFailed = useAgentMemoryStore((state) => state.globalFailed);
|
||||||
const projectFailed = useAgentMemoryStore((state) => state.projectFailed);
|
const projectFailed = useAgentMemoryStore((state) => state.projectFailed);
|
||||||
const deleteEntry = useAgentMemoryStore((state) => state.deleteEntry);
|
const deleteEntry = useAgentMemoryStore((state) => state.deleteEntry);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { resolveProjectContextId, type ProjectRef, type ProjectTodoItem } from '@/lib/projectContextApi';
|
import { resolveProjectContextId, type ProjectRef, type ProjectTodoItem } from '@/lib/projectContextApi';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
import { selectProjectMemoryForPath, useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
||||||
import { countHighlightedMemories, memoryViewKey } from '@/lib/agentMemoryBadges';
|
import { countHighlightedMemories, memoryViewKey } from '@/lib/agentMemoryBadges';
|
||||||
import { EMPTY_PROJECT_CONTEXT_ENTRY, useProjectContextStore } from '@/stores/useProjectContextStore';
|
import { EMPTY_PROJECT_CONTEXT_ENTRY, useProjectContextStore } from '@/stores/useProjectContextStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
@@ -133,7 +133,9 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
const memoryDisabledByServer = useAgentMemoryStore((state) => state.disabled);
|
const memoryDisabledByServer = useAgentMemoryStore((state) => state.disabled);
|
||||||
const memoryVisible = memoryEnabled && !memoryDisabledByServer;
|
const memoryVisible = memoryEnabled && !memoryDisabledByServer;
|
||||||
const globalMemory = useAgentMemoryStore((state) => state.global);
|
const globalMemory = useAgentMemoryStore((state) => state.global);
|
||||||
const projectMemory = useAgentMemoryStore((state) => state.project);
|
const projectMemory = useAgentMemoryStore(
|
||||||
|
(state) => selectProjectMemoryForPath(state, projectRef?.path ?? null),
|
||||||
|
);
|
||||||
|
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const storedTab = useUIStore((state) => state.projectContextTab);
|
const storedTab = useUIStore((state) => state.projectContextTab);
|
||||||
|
|||||||
@@ -13,12 +13,10 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
|
||||||
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
|
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
|
||||||
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { useProjectContextOwner } from '@/hooks/useProjectContextOwner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directory is a parameter rather than read from `useEffectiveDirectory`,
|
* The directory is a parameter rather than read from `useEffectiveDirectory`,
|
||||||
@@ -29,18 +27,9 @@ export const useAgentMemorySync = (directory: string | null): void => {
|
|||||||
const enabled = useUIStore((state) => (
|
const enabled = useUIStore((state) => (
|
||||||
state.agentMemoryFeatureAvailable && state.agentMemoryToolEnabled
|
state.agentMemoryFeatureAvailable && state.agentMemoryToolEnabled
|
||||||
));
|
));
|
||||||
const projects = useProjectsStore((state) => state.projects);
|
|
||||||
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
|
||||||
const effectiveDirectory = directory ?? '';
|
|
||||||
const load = useAgentMemoryStore((state) => state.load);
|
const load = useAgentMemoryStore((state) => state.load);
|
||||||
|
const owner = useProjectContextOwner(directory);
|
||||||
const projectPath = React.useMemo(() => {
|
const projectPath = owner?.path ?? null;
|
||||||
if (!effectiveDirectory) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const resolved = resolveProjectForSessionDirectory(projects, availableWorktreesByProject, effectiveDirectory);
|
|
||||||
return resolved?.path ?? null;
|
|
||||||
}, [availableWorktreesByProject, effectiveDirectory, projects]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
|
||||||
|
import { resolveProjectContextOwner } from './useProjectContextOwner';
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
{ id: 'openchamber', path: '/workspace/openchamber', label: 'OpenChamber' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('resolveProjectContextOwner', () => {
|
||||||
|
test('resolves a managed chat directory to the Chats root instead of the active project', () => {
|
||||||
|
const owner = resolveProjectContextOwner({
|
||||||
|
projects,
|
||||||
|
worktreesByProject: new Map(),
|
||||||
|
directory: '/Users/test/.config/openchamber/chats/2026-08-27/session-a',
|
||||||
|
activeProjectId: 'openchamber',
|
||||||
|
chatDraftOpen: false,
|
||||||
|
chatDraftTarget: 'project',
|
||||||
|
homeDirectory: '/Users/test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(owner).toEqual({
|
||||||
|
id: CHAT_DRAFT_PROJECT_ID,
|
||||||
|
path: '/Users/test/.config/openchamber/chats',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves a worktree session to its owning project', () => {
|
||||||
|
const owner = resolveProjectContextOwner({
|
||||||
|
projects,
|
||||||
|
worktreesByProject: new Map([
|
||||||
|
['/workspace/openchamber', [{
|
||||||
|
path: '/workspace/openchamber-feature',
|
||||||
|
projectDirectory: '/workspace/openchamber',
|
||||||
|
branch: 'feature',
|
||||||
|
label: 'feature',
|
||||||
|
}]],
|
||||||
|
]),
|
||||||
|
directory: '/workspace/openchamber-feature',
|
||||||
|
activeProjectId: null,
|
||||||
|
chatDraftOpen: false,
|
||||||
|
chatDraftTarget: 'project',
|
||||||
|
homeDirectory: '/Users/test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(owner).toEqual({ id: 'openchamber', path: '/workspace/openchamber' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory } from '@/lib/chatDirectories';
|
||||||
|
import { normalizePath } from '@/lib/pathNormalization';
|
||||||
|
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
||||||
|
import type { ProjectRef } from '@/lib/projectContextApi';
|
||||||
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
|
import type { WorktreeMetadata } from '@/types/worktree';
|
||||||
|
import type { ProjectEntry } from '@/lib/api/types';
|
||||||
|
|
||||||
|
interface ProjectContextOwnerInput {
|
||||||
|
projects: ProjectEntry[];
|
||||||
|
worktreesByProject: Map<string, WorktreeMetadata[]>;
|
||||||
|
directory: string | null;
|
||||||
|
activeProjectId: string | null;
|
||||||
|
chatDraftOpen: boolean;
|
||||||
|
chatDraftTarget: 'chat' | 'project';
|
||||||
|
homeDirectory: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveProjectContextOwner = ({
|
||||||
|
projects,
|
||||||
|
worktreesByProject,
|
||||||
|
directory,
|
||||||
|
activeProjectId,
|
||||||
|
chatDraftOpen,
|
||||||
|
chatDraftTarget,
|
||||||
|
homeDirectory,
|
||||||
|
}: ProjectContextOwnerInput): ProjectRef | null => {
|
||||||
|
const chatsRoot = getChatsRootFromDirectory(directory) ?? getChatsRootForHome(homeDirectory);
|
||||||
|
const normalizedDirectory = normalizePath(directory);
|
||||||
|
const normalizedChatsRoot = normalizePath(chatsRoot);
|
||||||
|
const ownsChats = chatDraftOpen
|
||||||
|
? chatDraftTarget === 'chat'
|
||||||
|
: Boolean(normalizedDirectory && normalizedChatsRoot && (
|
||||||
|
normalizedDirectory === normalizedChatsRoot || normalizedDirectory.startsWith(`${normalizedChatsRoot}/`)
|
||||||
|
));
|
||||||
|
|
||||||
|
if (ownsChats && chatsRoot) {
|
||||||
|
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionProject = resolveProjectForSessionDirectory(projects, worktreesByProject, directory);
|
||||||
|
if (sessionProject) {
|
||||||
|
return { id: sessionProject.id, path: sessionProject.path };
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProject = projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
|
||||||
|
return activeProject ? { id: activeProject.id, path: activeProject.path } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The single owner used by Project knowledge and agent-memory synchronization. */
|
||||||
|
export const useProjectContextOwner = (directory: string | null): ProjectRef | null => {
|
||||||
|
const projects = useProjectsStore((state) => state.projects);
|
||||||
|
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
||||||
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||||
|
const worktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||||
|
const chatDraftOpen = useSessionUIStore((state) => state.newSessionDraft.open);
|
||||||
|
const chatDraftTarget = useSessionUIStore((state) => state.newSessionDraft.target);
|
||||||
|
|
||||||
|
return React.useMemo(() => resolveProjectContextOwner({
|
||||||
|
projects,
|
||||||
|
worktreesByProject,
|
||||||
|
directory,
|
||||||
|
activeProjectId,
|
||||||
|
chatDraftOpen,
|
||||||
|
chatDraftTarget,
|
||||||
|
homeDirectory,
|
||||||
|
}), [
|
||||||
|
activeProjectId,
|
||||||
|
chatDraftOpen,
|
||||||
|
chatDraftTarget,
|
||||||
|
directory,
|
||||||
|
homeDirectory,
|
||||||
|
projects,
|
||||||
|
worktreesByProject,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -21,6 +21,10 @@ interface MemoryReadResult {
|
|||||||
projectFailed: boolean;
|
projectFailed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PendingMemoryRead {
|
||||||
|
resolve?: (result: MemoryReadResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swappable implementations rather than mock helpers: each test states the one
|
* Swappable implementations rather than mock helpers: each test states the one
|
||||||
* behaviour it needs.
|
* behaviour it needs.
|
||||||
@@ -45,7 +49,7 @@ mock.module('@/lib/agentMemoryApi', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { useAgentMemoryStore } = await import('./useAgentMemoryStore');
|
const { selectProjectMemoryForPath, useAgentMemoryStore } = await import('./useAgentMemoryStore');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useAgentMemoryStore.getState().reset();
|
useAgentMemoryStore.getState().reset();
|
||||||
@@ -86,6 +90,38 @@ describe('load', () => {
|
|||||||
expect(state.error).toBe('offline');
|
expect(state.error).toBe('offline');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not expose the previous project's memories under the Chats owner", async () => {
|
||||||
|
await useAgentMemoryStore.getState().load('/workspace/openchamber');
|
||||||
|
|
||||||
|
const pending: PendingMemoryRead = {};
|
||||||
|
readImpl = () => new Promise((resolve) => {
|
||||||
|
pending.resolve = resolve;
|
||||||
|
});
|
||||||
|
const chatsPath = '/Users/test/.config/openchamber/chats';
|
||||||
|
const loadingChats = useAgentMemoryStore.getState().load(chatsPath);
|
||||||
|
|
||||||
|
const switched = useAgentMemoryStore.getState();
|
||||||
|
expect(selectProjectMemoryForPath(switched, chatsPath)).toEqual([]);
|
||||||
|
expect(switched.projectPath).toBe(chatsPath);
|
||||||
|
|
||||||
|
pending.resolve?.({ global: [entry({ id: 'g1' })], project: [], globalFailed: false, projectFailed: false });
|
||||||
|
await loadingChats;
|
||||||
|
|
||||||
|
expect(selectProjectMemoryForPath(useAgentMemoryStore.getState(), chatsPath)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a failed load for a new owner stays distinct from an empty project', async () => {
|
||||||
|
await useAgentMemoryStore.getState().load('/workspace/openchamber');
|
||||||
|
readImpl = async () => { throw new Error('offline'); };
|
||||||
|
|
||||||
|
await useAgentMemoryStore.getState().load('/Users/test/.config/openchamber/chats');
|
||||||
|
|
||||||
|
const state = useAgentMemoryStore.getState();
|
||||||
|
expect(state.project).toEqual([]);
|
||||||
|
expect(state.projectFailed).toBe(true);
|
||||||
|
expect(state.error).toBe('offline');
|
||||||
|
});
|
||||||
|
|
||||||
test('a disabled feature clears the lists rather than reporting an error', async () => {
|
test('a disabled feature clears the lists rather than reporting an error', async () => {
|
||||||
await useAgentMemoryStore.getState().load('/tmp/project');
|
await useAgentMemoryStore.getState().load('/tmp/project');
|
||||||
readImpl = async () => { throw new AgentMemoryDisabledError(); };
|
readImpl = async () => { throw new AgentMemoryDisabledError(); };
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ const EMPTY_STATE = {
|
|||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EMPTY_MEMORY: AgentMemoryEntry[] = [];
|
||||||
|
|
||||||
|
/** Never expose one owner's project entries under another owner's heading. */
|
||||||
|
export const selectProjectMemoryForPath = (
|
||||||
|
state: AgentMemoryState,
|
||||||
|
projectPath: string | null,
|
||||||
|
): AgentMemoryEntry[] => state.projectPath === projectPath ? state.project : EMPTY_MEMORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only the newest load may write to the store. Turning the feature back on
|
* Only the newest load may write to the store. Turning the feature back on
|
||||||
* fires a load before the setting has finished being written, so an older
|
* fires a load before the setting has finished being written, so an older
|
||||||
@@ -93,13 +101,20 @@ export const useAgentMemoryStore = create<AgentMemoryState>((set, get) => ({
|
|||||||
|
|
||||||
load: async (projectPath) => {
|
load: async (projectPath) => {
|
||||||
const requestId = ++loadSequence;
|
const requestId = ++loadSequence;
|
||||||
set({ loading: true, projectPath });
|
const previous = get();
|
||||||
|
const ownerChanged = previous.projectPath !== projectPath;
|
||||||
|
if (ownerChanged) {
|
||||||
|
set({ loading: true, projectPath, project: [], projectFailed: false });
|
||||||
|
} else {
|
||||||
|
set({ loading: true, projectPath });
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const snapshot = await fetchAgentMemory(projectPath);
|
const snapshot = await fetchAgentMemory(projectPath);
|
||||||
if (requestId !== loadSequence) return;
|
if (requestId !== loadSequence) return;
|
||||||
|
const current = get();
|
||||||
set({
|
set({
|
||||||
global: snapshot.global,
|
global: snapshot.globalFailed ? current.global : snapshot.global,
|
||||||
project: snapshot.project,
|
project: snapshot.projectFailed ? current.project : snapshot.project,
|
||||||
projectPath,
|
projectPath,
|
||||||
globalFailed: snapshot.globalFailed,
|
globalFailed: snapshot.globalFailed,
|
||||||
projectFailed: snapshot.projectFailed,
|
projectFailed: snapshot.projectFailed,
|
||||||
@@ -119,7 +134,12 @@ export const useAgentMemoryStore = create<AgentMemoryState>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Whatever was loaded before stays. Only the error is new.
|
// Whatever was loaded before stays. Only the error is new.
|
||||||
set({ loading: false, error: errorMessage(error, 'Failed to load agent memory') });
|
set({
|
||||||
|
loading: false,
|
||||||
|
globalFailed: true,
|
||||||
|
projectFailed: true,
|
||||||
|
error: errorMessage(error, 'Failed to load agent memory'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -156,4 +176,3 @@ export const useAgentMemoryStore = create<AgentMemoryState>((set, get) => ({
|
|||||||
set({ ...EMPTY_STATE });
|
set({ ...EMPTY_STATE });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user