Skip to content
Closed
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
3 changes: 3 additions & 0 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,9 @@ func appMint(workspace string) (string, error) {

// appAgentID is a conversation's id from its transcript: the folder's name.
func appAgentID(transcript string) string {
if dir, ok := session.FolderOf(filepath.Clean(transcript)); ok {
return (session.Place{Dir: dir}).ID()
}
return filepath.Base(filepath.Dir(filepath.Clean(transcript)))
}

Expand Down
92 changes: 92 additions & 0 deletions cmd/codeaf/appidentity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"testing"
"time"

"github.com/Agent-Field/codeaf/internal/appwire"
"github.com/Agent-Field/codeaf/internal/cell"
)

func TestAppIndependentChatsKeepTheirOwnTranscripts(t *testing.T) {
for _, cells := range []bool{false, true} {
name := "legacy"
if cells {
name = "cells"
}
t.Run(name, func(t *testing.T) { testAppIndependentChats(t, cells) })
}
}

func testAppIndependentChats(t *testing.T, cells bool) {
_, workspace := wakeEnvironment(t, resolvedTempDir(t))
t.Cleanup(cell.UseAvailabilityInTests(cells))
rec := newAppRecorder()
backend := newAppBackend(rec, appOpenInProcess)
defer backend.Close()

first, err := mustCreate(t, backend, appwire.CreateAsk{Workspace: workspace})
if err != nil {
t.Fatal(err)
}
turn, err := backend.Send(first.ID, "explain billing migrations", "billing-first")
if err != nil {
t.Fatal(err)
}
rec.waitFor(t, 10*time.Second, func(_ string, data any) bool {
done, ok := data.(appwire.TurnDone)
return ok && done.Agent == first.ID && done.Turn == turn && done.OK
})
second, err := mustCreate(t, backend, appwire.CreateAsk{Workspace: workspace})
if err != nil {
t.Fatal(err)
}
if first.ID == second.ID {
t.Fatalf("two new chats share identity %q", first.ID)
}
turn, err = backend.Send(second.ID, "explain sourdough starters", "sourdough-first")
if err != nil {
t.Fatal(err)
}
rec.waitFor(t, 10*time.Second, func(_ string, data any) bool {
done, ok := data.(appwire.TurnDone)
return ok && done.Agent == second.ID && done.Turn == turn && done.OK
})
if err := backend.CloseAgent(first.ID); err != nil {
t.Fatal(err)
}
if _, err := backend.OpenAgent(first.ID); err != nil {
t.Fatalf("reopen billing: %v", err)
}
turn, err = backend.Send(first.ID, "which billing risk comes first?", "billing-followup")
if err != nil {
t.Fatalf("billing follow-up: %v", err)
}
rec.waitFor(t, 10*time.Second, func(_ string, data any) bool {
done, ok := data.(appwire.TurnDone)
return ok && done.Agent == first.ID && done.Turn == turn && done.OK
})
turn, err = backend.Send(second.ID, "how often should I feed the starter?", "sourdough-followup")
if err != nil {
t.Fatalf("sourdough follow-up: %v", err)
}
rec.waitFor(t, 10*time.Second, func(_ string, data any) bool {
done, ok := data.(appwire.TurnDone)
return ok && done.Agent == second.ID && done.Turn == turn && done.OK
})
for _, chat := range []struct{ id, text, followup string }{{first.ID, "explain billing migrations", "which billing risk comes first?"}, {second.ID, "explain sourdough starters", "how often should I feed the starter?"}} {
entries, _, err := backend.Entries(chat.id, "", appwire.MaxEntries)
if err != nil {
t.Fatal(err)
}
var words []string
for _, entry := range entries {
if entry.Kind == appwire.EntryUser {
words = append(words, entry.Text)
}
}
if len(words) != 2 || words[0] != chat.text || words[1] != chat.followup {
t.Fatalf("chat %s has user messages %q, want %q and %q", chat.id, words, chat.text, chat.followup)
}
}
}
16 changes: 16 additions & 0 deletions docs/changes/unreleased/1965-app-chat-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
kind: fixed
title: Desktop chats keep independent identities and transcripts with cell storage
pr: 1965
surface: [engine]
invalidates: []
---

The desktop backend now reads the conversation folder through the session layout
API. Cell transcripts live inside `.cell/`; treating that directory as the agent
ID gave every new chat the same identity. New chats now retain separate records,
transcripts and ownership when another chat opens or the first chat is reopened.
Legacy conversation folders retain their existing identities.

This prevents new collisions. It does not repair desktop tabs or drafts saved
with the old `.cell` identity.