refactor(web): resolve pico sessions from scope metadata
This commit is contained in:
parent
59dee895fc
commit
53482a17bc
2 changed files with 102 additions and 73 deletions
|
|
@ -44,25 +44,19 @@ type sessionListItem struct {
|
||||||
Updated string `json:"updated"`
|
Updated string `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// picoSessionPrefix is the key prefix used by the gateway's routing for Pico
|
// legacyPicoSessionPrefix is the legacy key prefix used by older Pico JSON/JSONL
|
||||||
// channel sessions. The full key format is:
|
// sessions before structured scope metadata existed.
|
||||||
//
|
|
||||||
// agent:main:pico:direct:pico:<session-uuid>
|
|
||||||
//
|
|
||||||
// The sanitized filename replaces ':' with '_', so on disk it becomes:
|
|
||||||
//
|
|
||||||
// agent_main_pico_direct_pico_<session-uuid>.json
|
|
||||||
const (
|
const (
|
||||||
picoSessionPrefix = "agent:main:pico:direct:pico:"
|
legacyPicoSessionPrefix = "agent:main:pico:direct:pico:"
|
||||||
maxSessionJSONLLineSize = 10 * 1024 * 1024 // 10 MB
|
maxSessionJSONLLineSize = 10 * 1024 * 1024 // 10 MB
|
||||||
maxSessionTitleRunes = 60
|
maxSessionTitleRunes = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
// extractPicoSessionID extracts the session UUID from a full session key.
|
// extractLegacyPicoSessionID extracts the session UUID from an old Pico key.
|
||||||
// Returns the UUID and true if the key matches the Pico session pattern.
|
// Returns the UUID and true if the key matches the Pico session pattern.
|
||||||
func extractPicoSessionID(key string) (string, bool) {
|
func extractLegacyPicoSessionID(key string) (string, bool) {
|
||||||
if strings.HasPrefix(key, picoSessionPrefix) {
|
if strings.HasPrefix(key, legacyPicoSessionPrefix) {
|
||||||
return strings.TrimPrefix(key, picoSessionPrefix), true
|
return strings.TrimPrefix(key, legacyPicoSessionPrefix), true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
@ -74,8 +68,7 @@ func sanitizeSessionKey(key string) string {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) readLegacySession(dir, sessionID string) (sessionFile, error) {
|
func (h *Handler) readLegacySession(path string) (sessionFile, error) {
|
||||||
path := filepath.Join(dir, sanitizeSessionKey(picoSessionPrefix+sessionID)+".json")
|
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sessionFile{}, err
|
return sessionFile{}, err
|
||||||
|
|
@ -184,6 +177,11 @@ type picoJSONLSessionRef struct {
|
||||||
Key string
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type picoLegacySessionRef struct {
|
||||||
|
ID string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
func extractPicoSessionIDFromScope(scope session.SessionScope) (string, bool) {
|
func extractPicoSessionIDFromScope(scope session.SessionScope) (string, bool) {
|
||||||
if !strings.EqualFold(strings.TrimSpace(scope.Channel), "pico") {
|
if !strings.EqualFold(strings.TrimSpace(scope.Channel), "pico") {
|
||||||
return "", false
|
return "", false
|
||||||
|
|
@ -208,15 +206,15 @@ func extractPicoSessionIDFromScope(scope session.SessionScope) (string, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionRefFromMeta(meta memory.SessionMeta) (picoJSONLSessionRef, bool) {
|
func sessionRefFromMeta(meta memory.SessionMeta) (picoJSONLSessionRef, bool) {
|
||||||
if sessionID, ok := extractPicoSessionID(meta.Key); ok {
|
if len(meta.Scope) == 0 {
|
||||||
|
if sessionID, ok := extractLegacyPicoSessionID(meta.Key); ok {
|
||||||
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
||||||
}
|
}
|
||||||
for _, alias := range meta.Aliases {
|
for _, alias := range meta.Aliases {
|
||||||
if sessionID, ok := extractPicoSessionID(alias); ok {
|
if sessionID, ok := extractLegacyPicoSessionID(alias); ok {
|
||||||
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(meta.Scope) == 0 {
|
|
||||||
return picoJSONLSessionRef{}, false
|
return picoJSONLSessionRef{}, false
|
||||||
}
|
}
|
||||||
var scope session.SessionScope
|
var scope session.SessionScope
|
||||||
|
|
@ -225,6 +223,14 @@ func sessionRefFromMeta(meta memory.SessionMeta) (picoJSONLSessionRef, bool) {
|
||||||
}
|
}
|
||||||
sessionID, ok := extractPicoSessionIDFromScope(scope)
|
sessionID, ok := extractPicoSessionIDFromScope(scope)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if legacySessionID, ok := extractLegacyPicoSessionID(meta.Key); ok {
|
||||||
|
return picoJSONLSessionRef{ID: legacySessionID, Key: meta.Key}, true
|
||||||
|
}
|
||||||
|
for _, alias := range meta.Aliases {
|
||||||
|
if legacySessionID, ok := extractLegacyPicoSessionID(alias); ok {
|
||||||
|
return picoJSONLSessionRef{ID: legacySessionID, Key: meta.Key}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
return picoJSONLSessionRef{}, false
|
return picoJSONLSessionRef{}, false
|
||||||
}
|
}
|
||||||
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
return picoJSONLSessionRef{ID: sessionID, Key: meta.Key}, true
|
||||||
|
|
@ -273,6 +279,51 @@ func (h *Handler) findPicoJSONLSession(dir, sessionID string) (picoJSONLSessionR
|
||||||
return picoJSONLSessionRef{}, os.ErrNotExist
|
return picoJSONLSessionRef{}, os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) findLegacyPicoSessions(dir string) ([]picoLegacySessionRef, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refs := make([]picoLegacySessionRef, 0)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(dir, entry.Name())
|
||||||
|
sess, err := h.readLegacySession(path)
|
||||||
|
if err != nil || isEmptySession(sess) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, ok := extractLegacyPicoSessionID(sess.Key)
|
||||||
|
if !ok || sessionID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[sessionID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[sessionID] = struct{}{}
|
||||||
|
refs = append(refs, picoLegacySessionRef{ID: sessionID, Path: path})
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) findLegacyPicoSession(dir, sessionID string) (picoLegacySessionRef, error) {
|
||||||
|
refs, err := h.findLegacyPicoSessions(dir)
|
||||||
|
if err != nil {
|
||||||
|
return picoLegacySessionRef{}, err
|
||||||
|
}
|
||||||
|
for _, ref := range refs {
|
||||||
|
if ref.ID == sessionID {
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return picoLegacySessionRef{}, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem {
|
func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem {
|
||||||
preview := ""
|
preview := ""
|
||||||
for _, msg := range sess.Messages {
|
for _, msg := range sess.Messages {
|
||||||
|
|
@ -365,8 +416,7 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
if _, err := os.ReadDir(dir); err != nil {
|
||||||
if err != nil {
|
|
||||||
// Directory doesn't exist yet = no sessions
|
// Directory doesn't exist yet = no sessions
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode([]sessionListItem{})
|
json.NewEncoder(w).Encode([]sessionListItem{})
|
||||||
|
|
@ -387,42 +437,18 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
if legacyRefs, findErr := h.findLegacyPicoSessions(dir); findErr == nil {
|
||||||
if entry.IsDir() {
|
for _, ref := range legacyRefs {
|
||||||
|
if _, exists := seen[ref.ID]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := entry.Name()
|
sess, loadErr := h.readLegacySession(ref.Path)
|
||||||
if strings.HasSuffix(name, ".meta.json") || filepath.Ext(name) != ".json" {
|
if loadErr != nil || isEmptySession(sess) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
seen[ref.ID] = struct{}{}
|
||||||
base := strings.TrimSuffix(name, ".json")
|
items = append(items, buildSessionListItem(ref.ID, sess))
|
||||||
if _, statErr := os.Stat(filepath.Join(dir, base+".jsonl")); statErr == nil {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, name))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var sess sessionFile
|
|
||||||
if err := json.Unmarshal(data, &sess); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isEmptySession(sess) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sessionID, ok := extractPicoSessionID(sess.Key)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := seen[sessionID]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
seen[sessionID] = struct{}{}
|
|
||||||
items = append(items, buildSessionListItem(sessionID, sess))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by updated descending (most recent first)
|
// Sort by updated descending (most recent first)
|
||||||
|
|
@ -487,7 +513,9 @@ func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
sess, err = h.readLegacySession(dir, sessionID)
|
if legacyRef, legacyErr := h.findLegacyPicoSession(dir, sessionID); legacyErr == nil {
|
||||||
|
sess, err = h.readLegacySession(legacyRef.Path)
|
||||||
|
}
|
||||||
if err == nil && isEmptySession(sess) {
|
if err == nil && isEmptySession(sess) {
|
||||||
err = os.ErrNotExist
|
err = os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
@ -560,8 +588,8 @@ func (h *Handler) handleDeleteSession(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
legacyPath := filepath.Join(dir, sanitizeSessionKey(picoSessionPrefix+sessionID)+".json")
|
if legacyRef, err := h.findLegacyPicoSession(dir, sessionID); err == nil {
|
||||||
if err := os.Remove(legacyPath); err != nil {
|
if err := os.Remove(legacyRef.Path); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
http.Error(w, "failed to delete session", http.StatusInternalServerError)
|
http.Error(w, "failed to delete session", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -569,6 +597,7 @@ func (h *Handler) handleDeleteSession(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
removed = true
|
removed = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !removed {
|
if !removed {
|
||||||
http.Error(w, "session not found", http.StatusNotFound)
|
http.Error(w, "session not found", http.StatusNotFound)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func TestHandleListSessions_JSONLStorage(t *testing.T) {
|
||||||
t.Fatalf("NewJSONLStore() error = %v", err)
|
t.Fatalf("NewJSONLStore() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey := picoSessionPrefix + "history-jsonl"
|
sessionKey := legacyPicoSessionPrefix + "history-jsonl"
|
||||||
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "Explain why the history API is empty after migration.",
|
Content: "Explain why the history API is empty after migration.",
|
||||||
|
|
@ -105,7 +105,7 @@ func TestHandleListSessions_TitleUsesTrimmedSummary(t *testing.T) {
|
||||||
t.Fatalf("NewJSONLStore() error = %v", err)
|
t.Fatalf("NewJSONLStore() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey := picoSessionPrefix + "summary-title"
|
sessionKey := legacyPicoSessionPrefix + "summary-title"
|
||||||
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "fallback preview",
|
Content: "fallback preview",
|
||||||
|
|
@ -161,7 +161,7 @@ func TestHandleGetSession_JSONLStorage(t *testing.T) {
|
||||||
t.Fatalf("NewJSONLStore() error = %v", err)
|
t.Fatalf("NewJSONLStore() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey := picoSessionPrefix + "detail-jsonl"
|
sessionKey := legacyPicoSessionPrefix + "detail-jsonl"
|
||||||
for _, msg := range []providers.Message{
|
for _, msg := range []providers.Message{
|
||||||
{Role: "user", Content: "first"},
|
{Role: "user", Content: "first"},
|
||||||
{Role: "assistant", Content: "second"},
|
{Role: "assistant", Content: "second"},
|
||||||
|
|
@ -302,7 +302,7 @@ func TestHandleDeleteSession_JSONLStorage(t *testing.T) {
|
||||||
t.Fatalf("NewJSONLStore() error = %v", err)
|
t.Fatalf("NewJSONLStore() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey := picoSessionPrefix + "delete-jsonl"
|
sessionKey := legacyPicoSessionPrefix + "delete-jsonl"
|
||||||
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
if err := store.AddFullMessage(nil, sessionKey, providers.Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "delete me",
|
Content: "delete me",
|
||||||
|
|
@ -339,7 +339,7 @@ func TestHandleGetSession_LegacyJSONFallback(t *testing.T) {
|
||||||
|
|
||||||
dir := sessionsTestDir(t, configPath)
|
dir := sessionsTestDir(t, configPath)
|
||||||
manager := session.NewSessionManager(dir)
|
manager := session.NewSessionManager(dir)
|
||||||
sessionKey := picoSessionPrefix + "legacy-json"
|
sessionKey := legacyPicoSessionPrefix + "legacy-json"
|
||||||
manager.AddMessage(sessionKey, "user", "legacy user")
|
manager.AddMessage(sessionKey, "user", "legacy user")
|
||||||
manager.AddMessage(sessionKey, "assistant", "legacy assistant")
|
manager.AddMessage(sessionKey, "assistant", "legacy assistant")
|
||||||
if err := manager.Save(sessionKey); err != nil {
|
if err := manager.Save(sessionKey); err != nil {
|
||||||
|
|
@ -364,7 +364,7 @@ func TestHandleSessions_FiltersEmptyJSONLFiles(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
dir := sessionsTestDir(t, configPath)
|
dir := sessionsTestDir(t, configPath)
|
||||||
base := filepath.Join(dir, sanitizeSessionKey(picoSessionPrefix+"empty-jsonl"))
|
base := filepath.Join(dir, sanitizeSessionKey(legacyPicoSessionPrefix+"empty-jsonl"))
|
||||||
if err := os.WriteFile(base+".jsonl", []byte{}, 0o644); err != nil {
|
if err := os.WriteFile(base+".jsonl", []byte{}, 0o644); err != nil {
|
||||||
t.Fatalf("WriteFile(jsonl) error = %v", err)
|
t.Fatalf("WriteFile(jsonl) error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue