From bb0bf46d65cea1f4e4f8b3f5c38bff6d8fc40d65 Mon Sep 17 00:00:00 2001 From: Santosh Date: Mon, 26 Oct 2026 11:30:23 -0400 Subject: [PATCH 1/2] fix(app): keep cell chat identities separate Cell transcripts live at /.cell/transcript.jsonl. appAgentID used the immediate parent, so every cell was .cell and later creates overwrote the held record. Use the existing layout-aware FolderOf and Place.ID. Legacy and flat fallback behavior stay intact. Laziness Protocol keeps the fix in the existing helper. Test Behavior Not Implementation covers real mint/open/send, independent transcripts, close/reopen and separate follow-ups with legacy and cell layouts. Prove It Works: the cell test failed with duplicate .cell before the fix; four focused backend tests now pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014ybUTo9ro1p9yNGyPHawz7 --- cmd/codeaf/app.go | 3 + cmd/codeaf/appidentity_test.go | 92 +++++++++++++++++++ .../changes/unreleased/0-app-chat-identity.md | 16 ++++ 3 files changed, 111 insertions(+) create mode 100644 cmd/codeaf/appidentity_test.go create mode 100644 docs/changes/unreleased/0-app-chat-identity.md diff --git a/cmd/codeaf/app.go b/cmd/codeaf/app.go index ee268f0530..741bb43aa0 100644 --- a/cmd/codeaf/app.go +++ b/cmd/codeaf/app.go @@ -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))) } diff --git a/cmd/codeaf/appidentity_test.go b/cmd/codeaf/appidentity_test.go new file mode 100644 index 0000000000..f4ad1c849e --- /dev/null +++ b/cmd/codeaf/appidentity_test.go @@ -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) + } + } +} diff --git a/docs/changes/unreleased/0-app-chat-identity.md b/docs/changes/unreleased/0-app-chat-identity.md new file mode 100644 index 0000000000..37c6603a39 --- /dev/null +++ b/docs/changes/unreleased/0-app-chat-identity.md @@ -0,0 +1,16 @@ +--- +kind: fixed +title: Desktop chats keep independent identities and transcripts with cell storage +pr: 0 +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. From 87535613d1cd2228b7c5d1fc9552120f2e8f4779 Mon Sep 17 00:00:00 2001 From: Santosh Date: Mon, 26 Oct 2026 11:31:25 -0400 Subject: [PATCH 2/2] docs: attach chat identity change entry to PR 1965 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014ybUTo9ro1p9yNGyPHawz7 --- .../{0-app-chat-identity.md => 1965-app-chat-identity.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{0-app-chat-identity.md => 1965-app-chat-identity.md} (98%) diff --git a/docs/changes/unreleased/0-app-chat-identity.md b/docs/changes/unreleased/1965-app-chat-identity.md similarity index 98% rename from docs/changes/unreleased/0-app-chat-identity.md rename to docs/changes/unreleased/1965-app-chat-identity.md index 37c6603a39..5835321304 100644 --- a/docs/changes/unreleased/0-app-chat-identity.md +++ b/docs/changes/unreleased/1965-app-chat-identity.md @@ -1,7 +1,7 @@ --- kind: fixed title: Desktop chats keep independent identities and transcripts with cell storage -pr: 0 +pr: 1965 surface: [engine] invalidates: [] ---