Skip to content
Merged
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
17 changes: 16 additions & 1 deletion cmd/codeaf/olderchats.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (o *olderChats) visit(ctx context.Context, dir string) (cell.Cell, session.
return cell.Cell{}, session.Meta{}, false
}
meta, _ := session.LoadMeta(dir)
if meta.LastUserAt.IsZero() || time.Since(meta.LastUserAt) > olderChatRecent || session.InUse(filepath.Join(dir, "transcript.jsonl")) {
if meta.LastUserAt.IsZero() || time.Since(meta.LastUserAt) > olderChatRecent || session.InUse(filepath.Join(dir, "transcript.jsonl")) || !olderChatCopyable(meta) {
return cell.Cell{}, session.Meta{}, false
}
c, err := o.copy(ctx, dir)
Expand All @@ -150,6 +150,18 @@ func (o *olderChats) visit(ctx context.Context, dir string) (cell.Cell, session.
return c, meta, true
}

// olderChatCopyable says a copy of the chat may seal its folder.
//
// A CHAT IN THE HOME FOLDER IS NOT COPIED. A copy's first seal has no record of
// what changed, so it walks the chat's whole folder, and the engine holds its
// one writer for the shared store while it does. Over the home folder that walk
// takes minutes, and every open chat's tools wait behind it until they time out.
// The home folder is not a project (session.HoldsHomeFolder); such a chat keeps
// opening where it is. A chat with its own folder is always copyable.
func olderChatCopyable(meta session.Meta) bool {
return meta.Owned || !session.HoldsHomeFolder(meta.Workspace)
}

// movablePlace resolves a completed copy or prepares one when the person opens
// an older chat. Selection runs on the existing asynchronous opening door.
func movablePlace(ctx context.Context, place session.Place) (session.Place, error) {
Expand All @@ -165,6 +177,9 @@ func movablePlace(ctx context.Context, place session.Place) (session.Place, erro
if _, err := os.Stat(filepath.Join(original, "transcript.jsonl")); os.IsNotExist(err) {
return place, nil
}
if meta, err := session.LoadMeta(original); err == nil && !olderChatCopyable(meta) {
return place, nil
}
c, err := copyOlderChat(ctx, original)
if err != nil {
return place, err
Expand Down
31 changes: 31 additions & 0 deletions cmd/codeaf/olderchats_retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,34 @@ func TestAFailedOlderChatCopyWaitsForTheChatToChange(t *testing.T) {
t.Fatalf("a chat that changed after its copy failed made %d copy attempts in total, want 2", attempts)
}
}

// TestAnOlderChatInTheHomeFolderIsNotCopied keeps the copier off a chat whose
// folder is the home folder. A copy seals the chat's folder, and sealing the home
// folder walks every file under it while holding the engine's one writer, so
// every open chat's tools waited behind it until they timed out.
func TestAnOlderChatInTheHomeFolderIsNotCopied(t *testing.T) {
t.Setenv(home.EnvVar, t.TempDir())
person := t.TempDir()
t.Setenv("HOME", person)
original := filepath.Join(home.Dir(), "v3", "projects", "home", "home-session")
if err := os.MkdirAll(original, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(original, "transcript.jsonl"), []byte(`{"type":"session","version":1,"id":"home-session"}`+"\n"), 0600); err != nil {
t.Fatal(err)
}
if err := session.SaveMeta(original, session.Meta{ID: "home-session", LastUserAt: time.Now(), Workspace: person}); err != nil {
t.Fatal(err)
}
attempts := 0
chats := olderChats{copy: func(context.Context, string) (cell.Cell, error) {
attempts++
return cell.Cell{}, nil
}}
if _, _, ok := chats.visit(t.Context(), original); ok {
t.Fatal("a chat in the home folder was reported as copied")
}
if attempts != 0 {
t.Fatalf("a chat in the home folder made %d copy attempts, want 0", attempts)
}
}
13 changes: 13 additions & 0 deletions docs/changes/unreleased/2002-older-chat-home-folder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
kind: fixed
title: Open chats no longer stall while an older chat in the home folder is copied
pr: 2002
surface: [chat, engine, docs]
invalidates:
- "After pairing, codeaf copied a recent older chat that worked in the home folder by saving the whole home folder; it now leaves that chat where it is."
- "While that copy ran, every open chat's tools failed with an engine capture-plan answer interrupted error; they no longer wait behind it."
---
A copy's first save walks the chat's whole folder while the engine's one
writer is held. Over the home folder that took minutes. The home folder is not
a project, so a chat that worked there keeps opening in place on this computer
and does not move to other devices.
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,12 @@ Once the chat arrives, you can send your next message immediately. The window ke

A running chat on the other computer never moves automatically. Neither does a named chat, `--host`, `--at` or `--session`, a chat with unsaved local edits, or a launch while another local window already has a chat open. If the service cannot be reached or another device got there first, Home says `other machines unreachable` or `another device continued this chat first` and keeps the local chat as it was.

## Why an older chat could not be copied
## Why an older chat could not be copied — symbolic links, home folder

An older chat whose folder is a symbolic link, or whose saved conversation state contains symbolic links, is left untouched. codeaf refuses to copy those links because writing through one could change the original. The original still opens with an older codeaf.

An older chat that worked in your home folder, or in a folder that holds it, is not copied either. A copy saves the whole folder as it stands, and your home folder is not one project: saving all of it takes minutes and holds up every open chat's tools meanwhile. That chat keeps opening where it is, on this computer only, and it does not move to your other devices. Chats with their own folder are copied as usual.

## A save has not reached the other computer yet — upload failed or another device continued first

A saved chat reaches your other computers only after its encrypted objects have
Expand Down
8 changes: 4 additions & 4 deletions internal/session/delegate_door.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func programCopyReceipt(ground string, via delegate.Delegate, record *TaskCopyRe
// or advising `git init` inside their dotfiles; it names the repository, the
// way the run's ending does ([ProgramFolderEnd.Sentence]).
func delegateFolderReceipt(ground string, via delegate.Delegate) string {
if _, _, outer, _ := programFolderOf(ground); outer != "" && !holdsHomeFolder(outer) {
if _, _, outer, _ := programFolderOf(ground); outer != "" && !HoldsHomeFolder(outer) {
return "It is " + via.Name + "'s: it works alone in " + ground + " itself; git ignores this folder inside " + outer +
", so codeaf cuts no branch there and commits nothing; its changes are there as it makes them."
} else if outer != "" {
Expand Down Expand Up @@ -357,7 +357,7 @@ func delegateStartedReceipt(id uint64, title, on, where, elsewhere string) strin
// what it changed — every file under the home folder, and a refusal from the
// first one macOS keeps to itself. instead is what the one refused can do.
func programHomeRefusal(program delegate.Delegate, dir, instead string) string {
if !program.LandsTree() || !holdsHomeFolder(dir) {
if !program.LandsTree() || !HoldsHomeFolder(dir) {
return ""
}
what := "holds your home folder"
Expand All @@ -367,8 +367,8 @@ func programHomeRefusal(program delegate.Delegate, dir, instead string) string {
return program.Name + " works in one project's folder, and " + dir + " " + what + "; " + instead
}

// holdsHomeFolder says dir is the person's home folder or a folder above it.
func holdsHomeFolder(dir string) bool {
// HoldsHomeFolder says dir is the person's home folder or a folder above it.
func HoldsHomeFolder(dir string) bool {
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" || strings.TrimSpace(dir) == "" {
return false
Expand Down
4 changes: 2 additions & 2 deletions internal/session/programfolder.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func PrepareProgramFolder(order ProgramFolderOrder) (*ProgramFolder, error) {
folder.release()
return nil, err
}
if outer != "" && !holdsHomeFolder(outer) {
if outer != "" && !HoldsHomeFolder(outer) {
folder.IgnoredOuter = outer
folder.Outer = ""
}
Expand Down Expand Up @@ -359,7 +359,7 @@ func programFolderOf(asked string) (dir string, repo bool, outer string, refusal
switch {
case !ok || !hasCommit(root):
return asked, false, "", ""
case holdsHomeFolder(root):
case HoldsHomeFolder(root):
// A REPOSITORY AT THE HOME FOLDER IS NOBODY'S PROJECT. A dotfiles
// repository there would otherwise have every folder under home read as
// its subfolder, and the program's branch cut in the person's dotfiles.
Expand Down
Loading