Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

## [Unreleased]

### 新增

- 记忆树支持手动新建主题/记忆,以及编辑已有记忆(replace/supersede,保留变更历史)。

### 修复

- 记忆树详情接入 `user_edit` lineage,人工修正后显示“人工修正的记忆”、修正前内容和原始来源上下文。

## [0.9.32] - 2026-09-06

### 新增
Expand Down
44 changes: 43 additions & 1 deletion dashboard/src/api/modules/memoryDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ export interface JournalItem {
target_atom_id?: string | null;
target_candidate_id?: string | null;
note?: string | null;
before?: Record<string, unknown> | null;
/** Short target memory/topic text enriched by the backend for specific action display. */
target_summary?: string | null;
/** Present on ``extract_run`` rows: structured stats for this extraction pass. */
after?: ExtractRunStats | null;
after?: ExtractRunStats | Record<string, unknown> | null;
}

export interface ListJournalResponse {
Expand Down Expand Up @@ -237,6 +238,19 @@ export interface RejectCandidateResponse {
status: "rejected";
}

export interface CreateAtomResponse {
atom: AtomItem;
entity: EntityItem;
created_entity: boolean;
status: "created";
}

export interface ReplaceAtomResponse {
old_atom_id: string;
atom: AtomItem;
status: "replaced" | "unchanged";
}

export interface LastExtractRun {
timestamp?: string;
session_id?: string | null;
Expand Down Expand Up @@ -450,6 +464,34 @@ export const memoryDashboardApi = {
{ method: "POST", body: JSON.stringify(body ?? {}) },
),

createAtom: (
aid: string,
body: {
assertion: string;
entity_id?: string;
entity_name?: string;
entity_type?: string;
kind?: AtomKind;
importance?: Importance;
confidence?: Confidence;
reason?: string;
},
) =>
request<CreateAtomResponse>(`${base(aid)}/atoms`, {
method: "POST",
body: JSON.stringify(body),
}),

replaceAtom: (
aid: string,
atomId: string,
body: { assertion: string; reason?: string },
) =>
request<ReplaceAtomResponse>(
`${base(aid)}/atoms/${encodeURIComponent(atomId)}:replace`,
{ method: "POST", body: JSON.stringify(body) },
),

// terminal aggregator (5 cards)
terminalAboutMe: (aid: string, limit = 5) =>
request<TerminalAtomResponse>(
Expand Down
39 changes: 39 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3493,6 +3493,45 @@
"deprecate": "Deprecate this memory",
"actions": "Actions"
},
"create": {
"title": "New memory",
"titleInTopic": "Add a memory under this topic",
"ok": "Save",
"success": "Memory added",
"failed": "Could not add memory: {{message}}",
"topic": "Topic",
"existingTopic": "Existing topic",
"newTopic": "New topic",
"pickTopic": "Choose a topic",
"topicRequired": "Please choose a topic",
"topicName": "Topic name",
"topicNameRequired": "Please enter a topic name",
"topicNamePlaceholder": "e.g. drink preferences",
"topicType": "Topic type",
"kind": "Memory type",
"assertion": "Memory",
"assertionRequired": "Please enter the memory",
"assertionPlaceholder": "e.g. prefers americano",
"addToTopicTip": "Add a memory under this topic"
},
"edit": {
"title": "Edit this memory",
"action": "Edit this memory",
"tooltip": "Edit this memory",
"description": "After saving, Octop will use the new text. The previous version stays in the change history.",
"ok": "Save",
"success": "Memory updated",
"failed": "Could not update memory: {{message}}",
"empty": "Memory text cannot be empty"
},
"lineage": {
"manualCorrection": "Manually corrected memory",
"beforeCorrection": "Before correction: {{assertion}}",
"originalContext": "Original source context:",
"manualEntry": "Manually added memory:",
"conversationSource": "Source conversation excerpt:",
"noOriginalContext": "No original source is available"
},
"deprecate": {
"title": "Deprecate this memory?",
"description": "Octop will no longer use this memory once it is deprecated. If it was recorded incorrectly, describe the correct content as the deprecation reason.",
Expand Down
39 changes: 39 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,45 @@
"deprecate": "弃用这条记忆",
"actions": "操作"
},
"create": {
"title": "新建记忆",
"titleInTopic": "在此主题下添加记忆",
"ok": "保存",
"success": "记忆已添加",
"failed": "添加失败:{{message}}",
"topic": "主题",
"existingTopic": "已有主题",
"newTopic": "新建主题",
"pickTopic": "选择主题",
"topicRequired": "请选择主题",
"topicName": "主题名称",
"topicNameRequired": "请填写主题名称",
"topicNamePlaceholder": "例如:饮品偏好",
"topicType": "主题类型",
"kind": "记忆类型",
"assertion": "记忆内容",
"assertionRequired": "请填写记忆内容",
"assertionPlaceholder": "例如:喜欢喝美式咖啡",
"addToTopicTip": "在此主题下添加记忆"
},
"edit": {
"title": "编辑这条记忆",
"action": "编辑这条记忆",
"tooltip": "编辑这条记忆",
"description": "保存后 Octop 会使用新内容;原来的记录会留在变更历史里。",
"ok": "保存",
"success": "记忆已更新",
"failed": "更新失败:{{message}}",
"empty": "记忆内容不能为空"
},
"lineage": {
"manualCorrection": "人工修正的记忆",
"beforeCorrection": "修正前:{{assertion}}",
"originalContext": "原始来源上下文:",
"manualEntry": "人工添加的记忆:",
"conversationSource": "来源对话片段:",
"noOriginalContext": "没有可展示的原始来源"
},
"deprecate": {
"title": "弃用这条记忆?",
"description": "弃用后 Octop 将不再使用这条记忆。如果是记录有误,建议先描述正确内容作为弃用原因。",
Expand Down
50 changes: 50 additions & 0 deletions dashboard/src/pages/Agent/Memory/AtomsList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import { makeAtom, listAtomsResp } from "../../../test/memoryFixtures";
vi.mock("../../../api/modules/memoryDashboard", () => ({
memoryDashboardApi: {
listAtoms: vi.fn(),
listEntities: vi
.fn()
.mockResolvedValue({ items: [], total: 0, has_more: false }),
listJournal: vi
.fn()
.mockResolvedValue({ items: [], total: 0, has_more: false }),
},
isAtomDeprecated: (a: { deprecated_at?: string | null }) =>
a.deprecated_at != null,
Expand Down Expand Up @@ -128,4 +134,48 @@ describe("<AtomsList />", () => {
).toBeGreaterThanOrEqual(1);
});
});

it("shows the audit record instead of presenting old evidence as the corrected assertion", async () => {
api.listAtoms.mockResolvedValue(
listAtomsResp([
makeAtom({
id: "atom-corrected",
assertion: "用户喜欢喝拿铁。",
verbatim_quote: "我每天早上都喝美式咖啡",
}),
]),
);
api.listJournal.mockResolvedValue({
items: [
{
id: "journal-edit",
timestamp: "2026-09-07T08:00:00Z",
action: "user_edit",
actor: "user",
target_atom_id: "atom-corrected",
before: { assertion: "用户喜欢喝美式咖啡。", atom_id: "atom-old" },
after: { assertion: "用户喜欢喝拿铁。", atom_id: "atom-corrected" },
},
],
total: 1,
has_more: false,
});

const user = userEvent.setup();
render(<AtomsList agentId="ZYWZTD" />);
await user.click(await screen.findByText("用户喜欢喝拿铁。"));

await waitFor(() => {
expect(api.listJournal).toHaveBeenCalledWith("ZYWZTD", {
action: "user_edit",
target_atom_id: "atom-corrected",
limit: 1,
});
});
expect(await screen.findByText(/人工修正的记忆/)).toBeInTheDocument();
expect(
screen.getByText("修正前:用户喜欢喝美式咖啡。"),
).toBeInTheDocument();
expect(screen.getByText(/原始来源上下文:/)).toBeInTheDocument();
});
});
Loading
Loading