diff --git a/Memory/src/service/read-model/episode.ts b/Memory/src/service/read-model/episode.ts index d07d25bf0..9c67f4f44 100644 --- a/Memory/src/service/read-model/episode.ts +++ b/Memory/src/service/read-model/episode.ts @@ -237,11 +237,11 @@ export class EpisodeReadModel { tool_call_start: number; tool_call_end: number; }; - const rawTurn = this.deps.repos.runtime.getRawTurn(span.raw_turn_id)!; + const rawTurn = this.deps.repos.runtime.getRawTurn(span.raw_turn_id); item.metadata.spanDetail = { toolCallStart: span.tool_call_start, toolCallEnd: span.tool_call_end, - toolCalls: rawTurn.toolCalls.slice(span.tool_call_start, span.tool_call_end + 1) + toolCalls: rawTurn?.toolCalls.slice(span.tool_call_start, span.tool_call_end + 1) ?? [] }; } return { diff --git a/Memory/tests/service/evolution/span-big-turn.test.ts b/Memory/tests/service/evolution/span-big-turn.test.ts index db72d1a66..75738d369 100644 --- a/Memory/tests/service/evolution/span-big-turn.test.ts +++ b/Memory/tests/service/evolution/span-big-turn.test.ts @@ -576,4 +576,71 @@ describe("MemoryService / evolution / span big turn", () => { ); db.close(); }); + + it("keeps span details readable after deleting the task that owned the raw turn", async () => { + const llm = createSpanBigTurnLlm([]); + const { db, service } = createTestService({ llm, skillLlm: llm }); + const namespace = { + source: "codex", + profileId: "jiang", + userId: "span-big-turn-deleted-task" + }; + const session = service.openSession({ namespace }); + const toolCalls = Array.from({ length: 11 }, (_, index) => ({ + id: `deleted-task-tool-${index}`, + name: "task_step", + input: { index } + })); + const completed = service.completeTurn("span-big-turn-deleted-task", { + namespace, + sessionId: session.sessionId, + query: "修复项目构建失败并完成测试验证", + answer: "已经定位依赖冲突,完成修复并通过构建与测试。", + toolCalls, + toolResults: toolCalls.map((call, index) => ({ + toolCallId: call.id, + name: call.name, + output: { ok: true, index } + })) + }); + service.closeSession(session.sessionId); + await service.feedback({ + namespace, + sessionId: session.sessionId, + episodeId: completed.episodeId, + l1MemoryId: completed.l1MemoryId, + channel: "explicit", + polarity: "positive", + magnitude: 1, + rationale: "复杂任务已经正确完成" + }); + await runWorkerRounds(service, 8); + const spanId = service.panelItems({ namespace, layer: "L1" }).items.find( + (item) => item.kind === "span" && item.title === "定位构建失败的根本原因" + )?.id; + expect(spanId).toBeDefined(); + + expect(service.deletePanelTask(completed.episodeId, { namespace })).toMatchObject({ + deletedMemoryIds: [completed.l1MemoryId] + }); + expect(db.db.prepare( + `SELECT COUNT(*) AS count FROM raw_turns WHERE id = ?` + ).get(completed.rawTurnId)).toEqual({ count: 0 }); + expect(service.panelItems({ namespace, layer: "L1" }).items.map((item) => item.id)).toContain(spanId); + + const detail = service.getMemory(spanId!, { namespace }); + expect(detail.item).toMatchObject({ + id: spanId, + kind: "span", + memoryLayer: "L1", + title: "定位构建失败的根本原因", + summary: "检查日志和依赖配置,确认失败来自版本冲突" + }); + expect(detail.item.metadata.spanDetail).toEqual({ + toolCallStart: 0, + toolCallEnd: 3, + toolCalls: [] + }); + db.close(); + }); });