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
53 changes: 12 additions & 41 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ package main
// idle conversations stay as indexed records, crew members included. Viewing,
// sending or waking opens one through the same per-agent gate. Listing agents,
// tasks and crews reads the disk. A conversation left alone for [appAgentIdle]
// without an open view is let go and rebuilt transparently on demand. Open
// views keep theirs only for the [appViewHolds] most recently used.
// without an open view is let go and rebuilt transparently on demand.

import (
"cmp"
Expand Down Expand Up @@ -76,21 +75,12 @@ import (
// appAgentIdle is how long a conversation the app holds may sit with no turn
// running, no question waiting and no open view before it is let go. Its
// journal stays on disk, its state still reads idle, and the next send opens
// it again. An explicitly open view keeps the conversation and its hold while
// it is among the [appViewHolds] most recently used.
// it again. An explicitly open view keeps the conversation and its hold.
const appAgentIdle = 10 * time.Minute

// appSweepEvery is how often the idle rule above is asked.
const appSweepEvery = 30 * time.Second

// appViewHolds is how many open views keep their idle conversation held: the
// most recently used ones. A window opens every chat it shows and never says
// when it stops showing one, so without a bound each chat shown in a session
// kept its journal, presence watch and timers until the app quit (#2471). An
// older idle view is let go like a background chat; its next read or send
// opens it again.
const appViewHolds = 3

// appCrewPoll is how often the teams file is looked at for crew.changed, once
// the app has asked for the crews at all. It is one stat when nothing moved.
const appCrewPoll = 2 * time.Second
Expand Down Expand Up @@ -789,8 +779,7 @@ type appAgent struct {
localNotices map[string]bool
windowMoved bool
// viewOpen preserves the person's open view through idle sweeps. Retiring
// it would let its next read immediately take back the released hold, so
// only views beyond the [appViewHolds] most recently used are let go.
// it would let its next read immediately take back the released hold.
viewOpen bool
// crewHeld keeps an established crew hold due after its engine closes.
crewHeld bool
Expand Down Expand Up @@ -4754,48 +4743,30 @@ func (b *appBackend) sweep() {
}

// sweepOnce is one look at the idle rule. A conversation is kept while a turn
// runs or a question waits for the person, whatever the clock says, and so is
// an open view among the [appViewHolds] most recently used.
// runs, a question waits for the person or a view stays open, whatever the clock says.
func (b *appBackend) sweepOnce() {
var syncing bool
b.locked(func() { syncing = b.drives != nil })
if syncing {
_, _ = b.SyncStatus()
}
now := b.now()
type candidate struct {
agent *appAgent
view bool
}
var idle []candidate
var idle []*appAgent
b.locked(func() {
var views []*appAgent
for _, a := range b.agents {
live := a.live
switch {
case live == nil:
case a.viewOpen:
views = append(views, a)
case !live.running && now.Sub(live.lastUse) >= appAgentIdle:
idle = append(idle, candidate{agent: a})
}
}
sort.Slice(views, func(i, j int) bool { return views[i].live.lastUse.After(views[j].live.lastUse) })
for _, a := range views[min(len(views), appViewHolds):] {
if !a.live.running && now.Sub(a.live.lastUse) >= appAgentIdle {
idle = append(idle, candidate{agent: a, view: true})
if live := a.live; live != nil && !a.viewOpen && !live.running && now.Sub(live.lastUse) >= appAgentIdle {
idle = append(idle, a)
}
}
})
for _, c := range idle {
b.sweepAgent(c.agent, now, c.view)
for _, a := range idle {
b.sweepAgent(a, now)
}
}

// sweepAgent lets one idle conversation go, unless something took its gate or
// woke it since the look. A view is let go only when view says it fell outside
// the most recently used holds.
func (b *appBackend) sweepAgent(a *appAgent, now time.Time, view bool) {
// woke it since the look.
func (b *appBackend) sweepAgent(a *appAgent, now time.Time) {
if !a.gate.TryLock() {
return
}
Expand All @@ -4804,7 +4775,7 @@ func (b *appBackend) sweepAgent(a *appAgent, now time.Time, view bool) {
keep := true
b.locked(func() {
live = a.live
keep = live == nil || (a.viewOpen && !view) || live.running || now.Sub(live.lastUse) < appAgentIdle
keep = live == nil || a.viewOpen || live.running || now.Sub(live.lastUse) < appAgentIdle
})
// A member of an open crew stays held while the crew is open.
if !keep && !b.liveNeedsYou(live) && !b.crewKeeps(a.view.ID) && !b.serviceKeeps(a.view.ID) {
Expand Down
102 changes: 0 additions & 102 deletions cmd/codeaf/app_view_holds_test.go

This file was deleted.

14 changes: 0 additions & 14 deletions docs/changes/unreleased/2472-idle-view-holds-bounded.md

This file was deleted.

11 changes: 11 additions & 0 deletions docs/changes/unreleased/2473-revert-view-holds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
kind: fixed
title: Desktop chats on screen stay held again, so another process cannot run a turn in one unseen
pr: 2473
surface: [app, engine, docs]
invalidates:
- "After #2472 the desktop engine let an open chat go once it was idle and outside the three most recently used, so a terminal codeaf on the same home could run a turn in a chat the window still showed without the window seeing it. An open view keeps its conversation and hold until it is closed again, as before #2472."
---

This reverts #2472. Per-chat file handles held by the engine (#2471) are
unbounded again until a fix keeps both the hold and a bound.
11 changes: 4 additions & 7 deletions internal/manual/chat/desktop-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,10 @@ once if that computer saved again first.

## Does an idle desktop chat keep its hold on the relay

A chat opened for viewing keeps its conversation and relay hold, including while
its tab is hidden, as long as it is one of the three most recently used open
chats. Heartbeats renew the same hold without releasing it or moving its fence.
Reading the chat does not cause ownership changes or repeat unchanged chat rows.
An older open chat left idle for ten minutes is let go like a background chat,
which frees its files and timers; using it again opens it through the normal
ownership check. Closing a chat lets it go at once.
A chat opened for viewing keeps its conversation and relay hold until it is
explicitly closed, including while its tab is hidden. Heartbeats renew the same
hold without releasing it or moving its fence. Reading the chat does not cause
ownership changes or repeat unchanged chat rows.

A conversation opened only for background work is let go after ten idle minutes;
the sweep checks every 30 seconds. It stays released until work needs it again.
Expand Down
Loading