feat(web): hide ephemeral media refs from persisted session history
This commit is contained in:
parent
ba5ef787b4
commit
19c35317ad
2 changed files with 90 additions and 39 deletions
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -407,10 +406,12 @@ func (h *Handler) findLegacyPicoSession(dir, sessionID string) (picoLegacySessio
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSessionListItem(sessionID string, sess sessionFile, toolFeedbackMaxArgsLength int) sessionListItem {
|
func buildSessionListItem(sessionID string, sess sessionFile, toolFeedbackMaxArgsLength int) sessionListItem {
|
||||||
|
transcript := visibleSessionMessages(sess.Messages, toolFeedbackMaxArgsLength)
|
||||||
|
|
||||||
preview := ""
|
preview := ""
|
||||||
for _, msg := range sess.Messages {
|
for _, msg := range transcript {
|
||||||
if msg.Role == "user" {
|
if msg.Role == "user" {
|
||||||
preview = sessionMessagePreview(msg)
|
preview = sessionChatMessagePreview(msg)
|
||||||
}
|
}
|
||||||
if preview != "" {
|
if preview != "" {
|
||||||
break
|
break
|
||||||
|
|
@ -423,13 +424,11 @@ func buildSessionListItem(sessionID string, sess sessionFile, toolFeedbackMaxArg
|
||||||
}
|
}
|
||||||
title := preview
|
title := preview
|
||||||
|
|
||||||
validMessageCount := len(visibleSessionMessages(sess.Messages, toolFeedbackMaxArgsLength))
|
|
||||||
|
|
||||||
return sessionListItem{
|
return sessionListItem{
|
||||||
ID: sessionID,
|
ID: sessionID,
|
||||||
Title: title,
|
Title: title,
|
||||||
Preview: preview,
|
Preview: preview,
|
||||||
MessageCount: validMessageCount,
|
MessageCount: len(transcript),
|
||||||
Created: sess.Created.Format(time.RFC3339),
|
Created: sess.Created.Format(time.RFC3339),
|
||||||
Updated: sess.Updated.Format(time.RFC3339),
|
Updated: sess.Updated.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
|
@ -450,11 +449,11 @@ func truncateRunes(s string, maxLen int) string {
|
||||||
return string(runes[:maxLen]) + "..."
|
return string(runes[:maxLen]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionMessageVisible(msg providers.Message) bool {
|
func sessionChatMessageVisible(msg sessionChatMessage) bool {
|
||||||
return strings.TrimSpace(msg.Content) != "" || len(msg.Media) > 0 || len(msg.Attachments) > 0
|
return strings.TrimSpace(msg.Content) != "" || len(msg.Media) > 0 || len(msg.Attachments) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionMessagePreview(msg providers.Message) string {
|
func sessionChatMessagePreview(msg sessionChatMessage) string {
|
||||||
if content := strings.TrimSpace(msg.Content); content != "" {
|
if content := strings.TrimSpace(msg.Content); content != "" {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
@ -484,13 +483,14 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case "user":
|
case "user":
|
||||||
if sessionMessageVisible(msg) {
|
chatMsg := sessionChatMessage{
|
||||||
transcript = append(transcript, sessionChatMessage{
|
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
Media: append([]string(nil), msg.Media...),
|
Media: append([]string(nil), msg.Media...),
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
})
|
}
|
||||||
|
if sessionChatMessageVisible(chatMsg) {
|
||||||
|
transcript = append(transcript, chatMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "assistant":
|
case "assistant":
|
||||||
|
|
@ -513,10 +513,6 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
|
||||||
// Pico web chat can persist both visible `message` tool output and a
|
// Pico web chat can persist both visible `message` tool output and a
|
||||||
// later plain assistant reply in the same turn. Hide only the fixed
|
// later plain assistant reply in the same turn. Hide only the fixed
|
||||||
// internal summary that marks handled tool delivery.
|
// internal summary that marks handled tool delivery.
|
||||||
if !sessionMessageVisible(msg) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
content := msg.Content
|
content := msg.Content
|
||||||
if assistantMessageInternalOnly(msg) {
|
if assistantMessageInternalOnly(msg) {
|
||||||
if len(attachments) == 0 {
|
if len(attachments) == 0 {
|
||||||
|
|
@ -525,12 +521,17 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
|
||||||
content = ""
|
content = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
transcript = append(transcript, sessionChatMessage{
|
chatMsg := sessionChatMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: content,
|
Content: content,
|
||||||
Media: append([]string(nil), msg.Media...),
|
Media: append([]string(nil), msg.Media...),
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
})
|
}
|
||||||
|
if !sessionChatMessageVisible(chatMsg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript = append(transcript, chatMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,12 +588,11 @@ func sessionAttachmentURL(attachment providers.Attachment) (string, bool) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(ref, "media://") {
|
if strings.HasPrefix(ref, "media://") {
|
||||||
refID := strings.TrimSpace(strings.TrimPrefix(ref, "media://"))
|
// Persisted session history must only expose durable attachment locations.
|
||||||
if refID == "" {
|
// media:// refs depend on the live in-memory MediaStore and may stop
|
||||||
|
// resolving after a restart or cleanup, so omit them from reopened history.
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return "/pico/media/" + url.PathEscape(refID), true
|
|
||||||
}
|
|
||||||
return ref, true
|
return ref, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ func TestHandleGetSession_JSONLStorage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
|
func TestHandleGetSession_HidesHandledToolAttachmentsBackedByMediaRefs(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|
@ -260,16 +260,63 @@ func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Messages []struct {
|
Messages []sessionChatMessage `json:"messages"`
|
||||||
Role string `json:"role"`
|
}
|
||||||
Content string `json:"content"`
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
Attachments []struct {
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
Type string `json:"type"`
|
}
|
||||||
URL string `json:"url"`
|
|
||||||
Filename string `json:"filename"`
|
if len(resp.Messages) != 1 {
|
||||||
ContentType string `json:"content_type"`
|
t.Fatalf("len(resp.Messages) = %d, want 1", len(resp.Messages))
|
||||||
} `json:"attachments"`
|
}
|
||||||
} `json:"messages"`
|
if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "send me the report" {
|
||||||
|
t.Fatalf("message = %#v, want only user request", resp.Messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleGetSession_ExposesHandledToolAttachmentsWithDurableURL(t *testing.T) {
|
||||||
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
dir := sessionsTestDir(t, configPath)
|
||||||
|
store, err := memory.NewJSONLStore(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewJSONLStore() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKey := legacyPicoSessionPrefix + "attachment-history-durable"
|
||||||
|
for _, msg := range []providers.Message{
|
||||||
|
{Role: "user", Content: "send me the report"},
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: handledToolResponseSummaryText,
|
||||||
|
Attachments: []providers.Attachment{{
|
||||||
|
Type: "file",
|
||||||
|
URL: "https://example.com/report.txt",
|
||||||
|
Filename: "report.txt",
|
||||||
|
ContentType: "text/plain",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if err := store.AddFullMessage(nil, sessionKey, msg); err != nil {
|
||||||
|
t.Fatalf("AddFullMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/sessions/attachment-history-durable", nil)
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Messages []sessionChatMessage `json:"messages"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
t.Fatalf("Unmarshal() error = %v", err)
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
|
@ -289,8 +336,12 @@ func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
|
||||||
if len(assistant.Attachments) != 1 {
|
if len(assistant.Attachments) != 1 {
|
||||||
t.Fatalf("len(assistant.Attachments) = %d, want 1", len(assistant.Attachments))
|
t.Fatalf("len(assistant.Attachments) = %d, want 1", len(assistant.Attachments))
|
||||||
}
|
}
|
||||||
if assistant.Attachments[0].URL != "/pico/media/attachment-1" {
|
if assistant.Attachments[0].URL != "https://example.com/report.txt" {
|
||||||
t.Fatalf("attachment url = %q, want %q", assistant.Attachments[0].URL, "/pico/media/attachment-1")
|
t.Fatalf(
|
||||||
|
"attachment url = %q, want %q",
|
||||||
|
assistant.Attachments[0].URL,
|
||||||
|
"https://example.com/report.txt",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if assistant.Attachments[0].Filename != "report.txt" {
|
if assistant.Attachments[0].Filename != "report.txt" {
|
||||||
t.Fatalf("attachment filename = %q, want %q", assistant.Attachments[0].Filename, "report.txt")
|
t.Fatalf("attachment filename = %q, want %q", assistant.Attachments[0].Filename, "report.txt")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue