Archive summarized messages instead of deleting them
要約で長期メモリに移したメッセージをDBから物理削除していたため、
一度要約されると人間も内容を確認できなくなっていた。削除をやめ、
messages に archived_at(要約に取り込まれた時刻)と memory_id
(どの要約に取り込まれたか)を持たせる。
モデルからは従来どおり見えない:
- get_context_messages() がアーカイブ済みを除外する
- maybe_summarize() の閾値判定も現役メッセージだけを数える
(ここを除外し忘れると件数が減らず再要約が繰り返される)
- backfill_tags() もアーカイブ済みは対象外にする
人間からは確認できる:
- GET /chat/sessions/{id}/messages がアーカイブ済みも archived_at
付きで返す。export/import もアーカイブ状態を保持する
- チャット画面では半透明で表示し、最後のアーカイブ済みメッセージの
直後に「ここから先がモデルに渡る」区切り線を出す
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,8 @@ export interface MessageRecord {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
created_at: string
|
||||
// 要約に取り込まれたメッセージ。モデルには渡らないが履歴としては残っている
|
||||
archived_at: string | null
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
@@ -83,6 +85,7 @@ export interface ImportPayload {
|
||||
role: string
|
||||
content: string
|
||||
created_at?: string | null
|
||||
archived_at?: string | null
|
||||
tags?: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { computed, ref, watch, nextTick } from 'vue'
|
||||
import { useChatStore } from '../stores/chat'
|
||||
import MarkdownContent from './MarkdownContent.vue'
|
||||
import type { Source } from '../api/client'
|
||||
@@ -7,6 +7,14 @@ import type { Source } from '../api/client'
|
||||
const store = useChatStore()
|
||||
const scrollEl = ref<HTMLElement | null>(null)
|
||||
|
||||
// 要約済みメッセージの最後の位置。ここに「ここから先がモデルに渡る」区切りを描く。
|
||||
const lastArchivedIndex = computed(() => {
|
||||
for (let i = store.messages.length - 1; i >= 0; i--) {
|
||||
if (store.messages[i].archived) return i
|
||||
}
|
||||
return -1
|
||||
})
|
||||
|
||||
function uniqueSources(sources: Source[]): Source[] {
|
||||
const seen = new Set<string>()
|
||||
return sources.filter((s) => {
|
||||
@@ -30,10 +38,10 @@ watch(
|
||||
<div v-if="store.messages.length === 0" class="empty">
|
||||
メッセージを送信してチャットを開始してください
|
||||
</div>
|
||||
<template v-for="(msg, i) in store.messages" :key="i">
|
||||
<div
|
||||
v-for="(msg, i) in store.messages"
|
||||
:key="i"
|
||||
:class="['message-wrap', msg.role]"
|
||||
:class="['message-wrap', msg.role, { archived: msg.archived }]"
|
||||
:title="msg.archived ? '要約済み: 内容は長期メモリに引き継がれ、この発言自体はモデルに渡りません' : undefined"
|
||||
>
|
||||
<div :class="['message', msg.role, { error: msg.error }]">
|
||||
<div class="bubble">
|
||||
@@ -65,6 +73,10 @@ watch(
|
||||
>{{ src.title || src.url }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="i === lastArchivedIndex" class="archive-divider">
|
||||
<span>ここまでは要約済み — 履歴として残っていますが、モデルには要約だけが渡ります</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -85,6 +97,23 @@ watch(
|
||||
.message-wrap { display: flex; flex-direction: column; gap: 4px; }
|
||||
.message-wrap.user { align-items: flex-end; }
|
||||
.message-wrap.assistant { align-items: flex-start; }
|
||||
/* 要約済み(モデルには渡らない)メッセージ。読めるが現役の発言と区別できる程度に落とす */
|
||||
.message-wrap.archived { opacity: 0.45; }
|
||||
.message-wrap.archived:hover { opacity: 0.75; }
|
||||
.archive-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
.archive-divider::before,
|
||||
.archive-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-top: 1px dashed #3a3a3a;
|
||||
}
|
||||
.message { display: flex; width: 100%; }
|
||||
.message.user { justify-content: flex-end; }
|
||||
.message.assistant { justify-content: flex-start; }
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Message {
|
||||
content: string
|
||||
pending?: boolean
|
||||
error?: boolean
|
||||
archived?: boolean
|
||||
tags?: string[]
|
||||
sources?: Source[]
|
||||
}
|
||||
@@ -55,7 +56,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const records = await fetchMessages(sessionId)
|
||||
messages.value = records
|
||||
.filter((r) => r.role === 'user' || r.role === 'assistant')
|
||||
.map((r) => ({ role: r.role as 'user' | 'assistant', content: r.content, tags: r.tags }))
|
||||
.map((r) => ({
|
||||
role: r.role as 'user' | 'assistant',
|
||||
content: r.content,
|
||||
tags: r.tags,
|
||||
archived: r.archived_at !== null,
|
||||
}))
|
||||
} catch (e) {
|
||||
pushToast(`メッセージ取得失敗: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user