Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c2e2f43
syncsetup: a paired computer lets go of an idle chat once everything …
AbirAbbas Nov 3, 2026
ea945b5
handoff: opening a released chat on a paired computer takes it withou…
AbirAbbas Nov 3, 2026
264eda7
tui3: a released window takes its chat back in place, and Home opens …
AbirAbbas Nov 3, 2026
ac27347
app: chat rows say when a chat can be opened here, and sends take a r…
AbirAbbas Nov 3, 2026
5dad3ae
synctake: a chat's copy folder keeps one bounded name however often i…
AbirAbbas Nov 3, 2026
f2d40e5
manual: an idle chat moves with you between paired computers
AbirAbbas Nov 3, 2026
eab2368
app, terminal: the new lock sections unlock from a defer
AbirAbbas Nov 3, 2026
7aa12fa
docs: number the change entry for #2287
AbirAbbas Nov 3, 2026
614c142
docs: the change entry names #2287
AbirAbbas Nov 3, 2026
942993a
syncsetup: an idle terminal chat releases again, and a late notice ne…
AbirAbbas Nov 3, 2026
315cd1f
syncsetup: ordinary home-state rounds wait for a competing flush again
AbirAbbas Nov 3, 2026
b96dda3
synctake: a copy folder is reused only for the exact copy root it names
AbirAbbas Nov 3, 2026
1aebd7d
tui3: a setting changed in a released terminal window retakes the hol…
AbirAbbas Nov 3, 2026
dd8f90f
session: a persistent setting in a released window goes through the o…
AbirAbbas Nov 3, 2026
0335dc0
app: settings in a released window retake the hold or are refused
AbirAbbas Nov 3, 2026
2e3935d
manual: settings after an idle release, and offline work in a held chat
AbirAbbas Nov 3, 2026
95c077b
syncsetup: Close always stops an ordinary writer, and a take-back cle…
AbirAbbas Nov 3, 2026
ce8e900
session: a person's durable write in a released window retakes the ho…
AbirAbbas Nov 3, 2026
ca395d0
cellstore: idle certification visits each path once, within fixed bounds
AbirAbbas Nov 3, 2026
8b0bfa6
app: a lent factory view keeps its model policy on the checked door
AbirAbbas Nov 3, 2026
e6dcd9f
manual: say sync service, and settings retake a released chat
AbirAbbas Nov 3, 2026
38d4382
syncdrive: a durable write reads the live drive without waiting on it…
AbirAbbas Nov 3, 2026
7d1fe24
syncsetup: taking a released chat back gets a write-sized budget, and…
AbirAbbas Nov 3, 2026
d7f023a
manual: what happens when taking a chat back cannot be confirmed
AbirAbbas Nov 3, 2026
453f3b1
Merge origin/app/v3 into feat/idle-chat-releases-to-relay
AbirAbbas Nov 10, 2026
b32e936
syncsetup: a self-released hold retakes at a new fence, never the rel…
AbirAbbas Nov 10, 2026
d8a1220
manual: an idle chat on paired computers opens directly; a held chat …
AbirAbbas Nov 10, 2026
df96db6
app: a desktop take says when the same computer picked the chat up again
AbirAbbas Nov 10, 2026
89871e2
tui3: Home's latest-chat offer follows presence, not a lapsed hold
AbirAbbas Nov 10, 2026
61cf89e
manual: same-computer take refusals and the presence-led latest-chat …
AbirAbbas Nov 10, 2026
2d2e434
syncsetup: a taken-back writer that idle-releases still counts as ins…
AbirAbbas Nov 10, 2026
2aa6cd7
manual: only Git-ignored files stay behind when a chat moves
AbirAbbas Nov 10, 2026
765b9fa
Revert "tui3: Home's latest-chat offer follows presence, not a lapsed…
AbirAbbas Nov 10, 2026
dbda6fd
Revert "manual: only Git-ignored files stay behind when a chat moves"
AbirAbbas Nov 10, 2026
db4583a
Merge origin/app/v3 (#2546) into feat/idle-chat-releases-to-relay
AbirAbbas Nov 10, 2026
5f7319f
tui3: the first Home open after a restart takes a chat no computer holds
AbirAbbas Nov 10, 2026
dee1cd1
tui3: opening a local chat never waits on, or fails on, the sync service
AbirAbbas Nov 10, 2026
e97be1b
tui3: rename the R5 restart test so it cannot spell the old product name
AbirAbbas Nov 10, 2026
4812dd3
tui3: add the R5b local-open tests the previous commit left out
AbirAbbas Nov 10, 2026
4125c03
tui3: with the sync service unreachable, Enter on your own chat opens it
AbirAbbas Nov 10, 2026
9f784d0
session: opening a chat that owes no turn never says "asking again"
AbirAbbas Nov 10, 2026
cb9a4d4
sync: only a released hold opens directly; a lapsed one still asks
AbirAbbas Nov 10, 2026
328b5d9
manual: a released chat opens directly; a lapsed one still asks Conti…
AbirAbbas Nov 10, 2026
b9fa2a2
tui3: the first Enter after the sync service returns sends, as it did…
AbirAbbas Nov 10, 2026
075f546
sync: a chat reopened after a failed close gets a fresh writer
AbirAbbas Nov 10, 2026
e480b0d
sync: read the drive and its closed state under a deferred unlock
AbirAbbas Nov 10, 2026
61ad993
fix(sync): an idle release that never left keeps holding the chat at …
AbirAbbas Nov 10, 2026
8723ae1
fix(chat): Enter on this computer's own live chat confirms the hold i…
AbirAbbas Nov 10, 2026
3662d90
sync: one ownership decision for work, and a lapsed fence never renew…
AbirAbbas Nov 10, 2026
4ecb459
chat: Enter after this computer's own hold lapsed takes the chat back…
AbirAbbas Nov 10, 2026
d2f99f8
sync: a call that finished after its hold expired drains or branches …
AbirAbbas Nov 10, 2026
4515297
chat: the first Enter after an outage checks again even after a faile…
AbirAbbas Nov 10, 2026
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
18 changes: 18 additions & 0 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,3 +756,21 @@ The macOS 60 s steady profile fell from 3.03% to 2.10% of a core. What remains
is the factory's lease reads on runner requests (17%) and the chat presence
beat (13%). The rig does not follow a live AgentField run, which the live
round's idle did; the AgentField write above is that path's share.

## Paired idle save certification

The idle-only `cellstore.SavedState` keeps the first fingerprint for each path
and stops climbing at an already visited ancestor. A proof admits at most
20,000 paths and 50,000 directory entries in total, 8 MiB per native output/diagnostic stream,
and 64 MiB of current/saved chat-file content reads. Directory reads request
only the remaining entry budget plus one. Native comparison uses its existing
30-second preparation deadline. Exceeding a budget retains the hold; it never
certifies a partial scope or limits ordinary saves or transfers.

`TestIdleRelease_R8_WideWorkspaceAndReceiptsAllocateLinearly` measures native
proofs over 100, 500 and 5,000 workspace files plus as many receipt records.
It caps retained paths and visited entries at `2*N+100`, and Go allocations at
`6 MiB + N*32,000` bytes. Before deduplication, 500+500 allocated 106,534,776
bytes; afterward it allocated 2,505,888 bytes. The 5,000+5,000 proof allocated
21,807,368 bytes. Bounds tests require refusal for paths, entries, reply size
and content bytes. Final metadata revalidation remains mandatory.
51 changes: 47 additions & 4 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1777,7 +1777,30 @@ func appSendUnsure(err error) bool {
}

// OpenAgent attaches a conversation to a saved agent and answers it.
func (b *appBackend) OpenAgent(id string) (appwire.Agent, error) { return b.openAgent(id, false) }
func (b *appBackend) OpenAgent(id string) (appwire.Agent, error) {
// Only a row already read for this paired identity can enable automatic
// pull. Ordinary local opens and single-device homes add no relay request.
row, found := b.chats.rowFor(id)
if found && row.CanOpen && !row.Here {
return b.takeAgent(appwire.TakeParams{Cell: row.Cell, Seen: row.Seen}, true)
}
return b.openAgent(id, false)
}

// rowFor is the last listed row for an agent id or a shared chat's cell id.
func (c *appChats) rowFor(id string) (appwire.ChatRow, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if row, found := c.byAgent[id]; found {
return row, true
}
for _, row := range c.rows {
if row.Cell == id {
return row, true
}
}
return appwire.ChatRow{}, false
}

// WarmAgent is OpenAgent for a window's guess at a view (a hover's prefetch):
// an agent in Closed or the archive is refused, never taken out again
Expand Down Expand Up @@ -2501,7 +2524,7 @@ func (b *appBackend) sendLocked(a *appAgent, text, clientID string, shown *appSe
func (b *appBackend) knownWorkRefusalGated(a *appAgent) error {
a.gate.Lock()
defer a.gate.Unlock()
return b.knownWorkRefusal(a)
return b.idleWorkRefusal(a)
}

// Retry sends the words of the agent's last failed turn again.
Expand Down Expand Up @@ -4134,7 +4157,13 @@ func (b *appBackend) SetModel(id, model string) (appwire.Agent, error) {
if live.conn.Lent {
return appwire.Agent{}, errLentStage("model")
}
live.conn.Conv.SetModel(model)
if checked, ok := live.conn.Conv.(interface{ SetModelChecked(string) error }); ok {
if err := checked.SetModelChecked(model); err != nil {
return appwire.Agent{}, appEngineError(err)
}
} else {
live.conn.Conv.SetModel(model)
}
if now := live.conn.Conv.Model(); now != "" {
model = now
}
Expand Down Expand Up @@ -4179,7 +4208,18 @@ func (b *appBackend) SetEffort(id, word string) (appwire.Agent, error) {
SetConversationEffort(string) bool
ConversationEffort() string
})
if !ok || !dial.SetConversationEffort(rung.String()) {
took := false
if ok {
if checked, yes := dial.(interface{ SetConversationEffortChecked(string) (bool, error) }); yes {
took, err = checked.SetConversationEffortChecked(rung.String())
if err != nil {
return appwire.Agent{}, appEngineError(err)
}
} else {
took = dial.SetConversationEffort(rung.String())
}
}
if !ok || !took {
return appwire.Agent{}, appwire.Errorf(appwire.CodeEngine, "this conversation has no thinking dial; update the engine and reconnect")
}
chosen := dial.ConversationEffort()
Expand Down Expand Up @@ -4909,6 +4949,9 @@ func appEngineError(err error) error {
}
var refusal *executor.WorkRefusal
if errors.As(err, &refusal) {
if refusal.Retryable {
return &appwire.Error{Code: appwire.CodeUnreachable, Message: refusal.Line}
}
location := &appwire.Location{Here: false, Line: refusal.Line}
if refusal.HolderID != "" {
location.Holder = &appwire.LocationHolder{ID: refusal.HolderID, Name: refusal.HolderName}
Expand Down
1 change: 1 addition & 0 deletions cmd/codeaf/app_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ func (b *appBackend) readChats(parent context.Context, s *syncsetup.Sync, feed d
_, installed := local[r.Cell]
here := r.Mine && (r.Status == chatlist.Here || installed)
row := appwire.ChatRow{Cell: r.Cell, Title: r.Title, Here: here, Status: string(r.Status), Seen: appwire.Seen{Device: r.DeviceID, Fence: r.Fence, Head: r.Head}, CanTake: !here && r.Head != ""}
row.CanOpen = r.AutoOpen && r.Head != ""
if service, ok := s.Dir.(interface{ HeadTakes() bool }); ok && !service.HeadTakes() {
row.Seen.Head = ""
}
Expand Down
38 changes: 19 additions & 19 deletions cmd/codeaf/app_idle_release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
a.locked(func() { idle = a.agents[agent.ID].live != nil && !a.agents[agent.ID].live.running })
return idle
})
appSyncWait(t, "paired idle hold released before sweeping", func() bool {
view, err := own.Dir.Cell(t.Context(), agent.ID)
return err == nil && view.Cell.Lease.Expires == 0
})
var transcript string
a.locked(func() { transcript = a.agents[agent.ID].transcript })
chat, err := cell.OpenAt(session.DirOf(transcript), agent.ID)
Expand All @@ -210,7 +214,7 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
if err != nil || head == nil {
t.Fatalf("idle setup head = %v, %v", head, err)
}
row := idleReleaseRow(t, wire, agent.ID, func(r appwire.ChatRow) bool { return r.Seen.Head == head.Turn.ID })
row := idleReleaseRow(t, wire, agent.ID, func(r appwire.ChatRow) bool { return r.Seen.Head == head.Turn.ID && r.Status == "idle" })
if leftover {
// Model the old engine's mismatch after its final checkpoint: the
// stopped job is in tasks.json, but its process file still says running.
Expand All @@ -234,7 +238,9 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
}
peer.waitFor(t, 10*time.Second, func(_ string, data any) bool {
changed, ok := data.(appwire.ChatChanged)
return ok && changed.Chat.Cell == agent.ID && changed.Chat.Seen == row.Seen && changed.Chat.Online != nil && *changed.Chat.Online
// Release preserves head and fence. Wait for its idle status too,
// rather than matching an earlier live row with the same receipt.
return ok && changed.Chat.Cell == agent.ID && changed.Chat.Seen == row.Seen && changed.Chat.Status == row.Status && changed.Chat.Online != nil && *changed.Chat.Online
})
peer.mu.Lock()
cursor := len(peer.events)
Expand Down Expand Up @@ -299,12 +305,8 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
if err != nil {
t.Fatal(err)
}
wantReleases := int32(1)
if viewed {
wantReleases = 0
}
live := v.Cell.Lease.Expires > v.Now
if live != viewed || v.Cell.Lease.Fence != row.Seen.Fence || acquires.Load() != 0 || releases.Load() != wantReleases {
if live || v.Cell.Lease.Fence != row.Seen.Fence || acquires.Load() != 0 || releases.Load() != 0 {
t.Fatalf("I1/I2 period %d: lease=%+v acquires=%d releases=%d", period+1, v.Cell.Lease, acquires.Load(), releases.Load())
}
if leftover {
Expand All @@ -314,10 +316,13 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
}
}
if viewed {
appSyncWait(t, "I1 open idle hold to renew beyond its original deadline", func() bool {
v, err := own.Dir.Cell(t.Context(), agent.ID)
return err == nil && v.Cell.Lease.Expires > before.Cell.Lease.Expires
})
// Keeping the open conversation no longer keeps its paired idle hold.
// The release keeps the same fence until the next work admission.
var kept bool
a.locked(func() { kept = a.agents[agent.ID].live != nil })
if !kept || before.Cell.Lease.Expires != 0 {
t.Fatal("I1 open reader was closed or still held its idle lease")
}
if acquires.Load() != 0 || releases.Load() != 0 {
t.Fatalf("I1 renewal acquired=%d released=%d", acquires.Load(), releases.Load())
}
Expand All @@ -326,14 +331,12 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
changes := 0
for _, data := range peer.data[cursor:] {
if changed, ok := data.(appwire.ChatChanged); ok && changed.Chat.Cell == agent.ID {
t.Logf("I2 row changed during quiet measurement: %+v", changed.Chat)
changes++
}
}
peer.mu.Unlock()
wantChanges := 1
if viewed {
wantChanges = 0
}
wantChanges := 0
if changes != wantChanges {
t.Fatalf("I2 idle chat emitted %d changes, want %d", changes, wantChanges)
}
Expand All @@ -351,7 +354,7 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
for range 2 {
time.Sleep(250 * time.Millisecond)
v, err := own.Dir.Cell(t.Context(), agent.ID)
if err != nil || v.Cell.Lease.Fence != row.Seen.Fence || acquires.Load() != 0 || releases.Load() != 1 {
if err != nil || v.Cell.Lease.Fence != row.Seen.Fence || acquires.Load() != 0 || releases.Load() != 0 {
t.Fatalf("B1/B3 restart acquired: lease=%+v acquires=%d releases=%d err=%v", v.Cell.Lease, acquires.Load(), releases.Load(), err)
}
}
Expand All @@ -368,9 +371,6 @@ func appIdleReleaseI1I2(t *testing.T, viewed bool, stopped ...bool) {
}
appTurnDoneFor(t, rec, turn)
wantFence, wantAcquires := row.Seen.Fence+1, int32(1)
if viewed {
wantFence, wantAcquires = row.Seen.Fence, 0
}
newRow := idleReleaseRow(t, wire, agent.ID, func(r appwire.ChatRow) bool { return r.Seen.Fence == wantFence && r.Seen.Head != row.Seen.Head })
if acquires.Load() != wantAcquires || newRow.Holder == nil || newRow.Holder.ID != own.Device.ID() {
t.Fatalf("I1 send: row=%+v acquires=%d", newRow, acquires.Load())
Expand Down
7 changes: 7 additions & 0 deletions cmd/codeaf/app_lend.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ func endsAView(door session.StopDoor) bool {
// model change has no error to return.
func (l lentAgent) SetModel(string) {}

// SetModelChecked preserves the legacy wrapper's model policy at the checked
// RPC door; the embedded agent's promoted setter must not bypass it.
func (l lentAgent) SetModelChecked(model string) error {
l.SetModel(model)
return nil
}

// SetSpendRail refuses: the stage's rail is the factory's.
func (l lentAgent) SetSpendRail(float64) error { return errLentStage("spend limit") }

Expand Down
62 changes: 54 additions & 8 deletions cmd/codeaf/app_take.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,19 @@ var (
appTakeSeen = func(ctx context.Context, s *syncsetup.Sync, id string, seen *directory.LeaseExpectation, onMove func(syncsetup.MoveProgress)) (syncsetup.Continued, error) {
return chatMover(s, onMove).TakeSeen(ctx, id, seen)
}
appTakeOpen = (*appBackend).OpenAgent
appTakeOpen = func(b *appBackend, id string) (appwire.Agent, error) {
return b.openAgent(id, false)
}
)

// TakeAgent moves a chat here. Its row says the move is queued, then how far
// it has got, and, when it did not move the chat, how it ended, so a client
// that stopped waiting still learns the outcome.
func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err error) {
func (b *appBackend) TakeAgent(p appwire.TakeParams) (appwire.Agent, error) {
return b.takeAgent(p, false)
}

func (b *appBackend) takeAgent(p appwire.TakeParams, available bool) (agent appwire.Agent, err error) {
if p.Cell == "" || p.Seen.Device == "" {
return appwire.Agent{}, appwire.Errorf(appwire.CodeBadParams, "a chat and its seen computer are required")
}
Expand Down Expand Up @@ -131,8 +137,11 @@ func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err e
return appwire.Agent{}, err
}
seen := &directory.LeaseExpectation{Device: p.Seen.Device, Fence: p.Seen.Fence, Head: p.Seen.Head}
if available && view.Cell.Lease.Expires != 0 {
return appwire.Agent{}, appAvailableRefusal(ctx, s, view.Cell.Lease)
}
if !seen.Matches(view.Cell) {
return appwire.Agent{}, appTakeError(directory.ErrLeaseHeld)
return appwire.Agent{}, appTakeRefusal(ctx, s, p.Cell, seen, directory.ErrLeaseHeld)
}
if view.Cell.Archived || view.Cell.ParentCell != "" {
return appwire.Agent{}, appwire.Errorf(appwire.CodeNotFound, "that chat is not available")
Expand All @@ -159,7 +168,7 @@ func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err e
if busy {
return appwire.Errorf(appwire.CodeBusy, "Finish this chat's work before continuing it here.")
}
if moved {
if available || moved {
b.release(existing)
}
return nil
Expand All @@ -169,17 +178,27 @@ func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err e
}
}
report, stopReport := b.moveReporter(p.Cell, move)
got, err := appTakeSeen(ctx, s, p.Cell, seen, report)
var got syncsetup.Continued
if available {
got, err = chatMover(s, report).OpenAvailable(ctx, p.Cell, seen)
} else {
got, err = appTakeSeen(ctx, s, p.Cell, seen, report)
}
stopReport()
if err != nil {
return appwire.Agent{}, err
if available && (errors.Is(err, directory.ErrLeaseHeld) || errors.Is(err, directory.ErrFenceStale)) {
if fresh, readErr := s.Dir.Cell(ctx, p.Cell); readErr == nil && fresh.Cell.Lease.Expires > fresh.Now {
return appwire.Agent{}, appAvailableRefusal(ctx, s, fresh.Cell.Lease)
}
}
return appwire.Agent{}, appTakeRefusal(ctx, s, p.Cell, seen, err)
}
// The chat is this computer's from here on, even if opening it fails.
b.chats.landTake(p.Cell, got.Taken.Driving.Fence)
// The continuer's already-here shortcut may observe a local acquisition
// that happened after our preflight read. It must not accept older consent.
if got.AlreadyHere && (p.Seen.Device != s.Device.ID() || !seen.Matches(directory.Cell{Head: got.Taken.Driving.Head, Lease: directory.Lease{Device: s.Device.ID(), Fence: got.Taken.Driving.Fence}})) {
return appwire.Agent{}, appTakeError(directory.ErrLeaseHeld)
return appwire.Agent{}, appTakeRefusal(ctx, s, p.Cell, seen, directory.ErrLeaseHeld)
}
root := got.Taken.Cell.Root
meta, err := session.LoadMeta(root)
Expand All @@ -193,7 +212,7 @@ func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err e
}
b.remember(session.SessionRow{ID: id, Dir: root, Transcript: transcript, Workspace: meta.Workspace, Title: meta.Title, Created: meta.Created, Model: meta.Model, ModelIdentity: meta.ModelIdentity})
a := b.agent(id)
b.installTaken(a, transcript, meta, !got.AlreadyHere, moved)
b.installTaken(a, transcript, meta, !got.AlreadyHere, available || moved)
location := &appwire.Location{Here: true, Cell: p.Cell, Seen: &appwire.Seen{Device: s.Device.ID(), Fence: got.Taken.Driving.Fence, Head: got.Taken.Driving.Head}, Holder: &appwire.LocationHolder{ID: s.Device.ID(), Name: got.Device}}
if service, ok := s.Dir.(interface{ HeadTakes() bool }); ok && !service.HeadTakes() {
location.Seen.Head = ""
Expand Down Expand Up @@ -231,6 +250,33 @@ func (b *appBackend) TakeAgent(p appwire.TakeParams) (agent appwire.Agent, err e
return agent, nil
}

// appTakeRefusal reads ownership again because the observed computer may have
// reclaimed its own idle release while this take was on the way.
func appTakeRefusal(ctx context.Context, s *syncsetup.Sync, id string, seen *directory.LeaseExpectation, err error) error {
if !errors.Is(err, directory.ErrLeaseHeld) && !errors.Is(err, directory.ErrFenceStale) {
return err
}
wire := appTakeError(err).(*appwire.Error)
if fresh, readErr := s.Dir.Cell(ctx, id); readErr == nil && fresh.Cell.Lease.Device == seen.Device && fresh.Cell.Lease.Fence > seen.Fence {
wire.Message = fmt.Sprintf(chatlist.PickedUpAgain, appHolderName(ctx, s, fresh.Cell.Lease.Device))
}
return wire
}

func appHolderName(ctx context.Context, s *syncsetup.Sync, device string) string {
name := chatlist.DeviceName(nil, device, nil)
if listing, err := s.Dir.List(ctx); err == nil {
key := directory.MetadataKey(s.Identity.CellKey())
name = chatlist.DeviceName(listing.Devices, device, func(sealed string) (string, error) { return directory.OpenName(key, sealed) })
}
return name
}

func appAvailableRefusal(ctx context.Context, s *syncsetup.Sync, lease directory.Lease) error {
name := appHolderName(ctx, s, lease.Device)
return appLocationRefusal(&appwire.Location{Holder: &appwire.LocationHolder{ID: lease.Device, Name: name}, Line: chatlist.Superseded(name)})
}

// installTaken points the agent at the chat a take installed. installed: the
// take replaced the saved conversation; moved: another computer held it.
// Under the agent's gate, as releaseForTake: a send waits for the reset
Expand Down
Loading
Loading