Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
1cbe5a3
tui3: an overview of open conversations
agentfield-bot Sep 23, 2026
0f00583
tui3: spaces live in the profile on an ordinary launch, and the new c…
agentfield-bot Sep 23, 2026
d01c05a
tui3: polish the conversations view painting
agentfield-bot Sep 23, 2026
34c8b3a
Merge branch 'task/wall-polish-paint' into feat/conversation-overview
agentfield-bot Sep 23, 2026
145b876
tui3: the wall opens on one press, scrolls by row and moves a little
agentfield-bot Sep 23, 2026
f48af81
Merge branch 'task/wall-polish-paint' into task/wall-polish-motion
agentfield-bot Sep 23, 2026
a0f2f06
Merge branch 'feat/conversation-overview' into task/wall-polish-motion
agentfield-bot Sep 23, 2026
b85bbc8
tui3: the wall tells the painter the pointer's row, and the hit helpe…
agentfield-bot Sep 23, 2026
767b9fb
tui3: the dock's signal test holds its flags on watchers nothing else…
agentfield-bot Sep 23, 2026
005602b
tui3: the wall's tile actions are labelled words, ? is help, and the …
agentfield-bot Sep 24, 2026
1f12208
tui3: spaces are called teams, in the code and on screen
agentfield-bot Sep 24, 2026
33fd9de
tui3: teams are known by id, can nest, and move from spaces.json to t…
agentfield-bot Sep 24, 2026
1ef4908
tui3: a new team is named once, from its conversations
agentfield-bot Sep 24, 2026
d4513fe
tui3: a conversation started while a team is shown joins it
agentfield-bot Sep 24, 2026
52abfb4
tui3: the strip's team chip is a switcher
agentfield-bot Sep 24, 2026
62393f4
manual: conversations and teams have a page of their own
agentfield-bot Sep 24, 2026
22cb944
tui3: the frame dumps show the naming card and the team switcher
agentfield-bot Sep 24, 2026
bc7eb55
session: ProposeTeams suggests teams for a person's conversations
agentfield-bot Sep 24, 2026
d8e3545
tui3: Organize suggests teams on the wall's All view
agentfield-bot Sep 24, 2026
c520c36
manual: Organize on the conversations and teams page
agentfield-bot Sep 24, 2026
e97e24e
docs: the design of conversations, teams, organize and the manager
agentfield-bot Sep 24, 2026
03bc2bb
teams: one store for teams, handles, the manager, Traffic and the digest
agentfield-bot Sep 24, 2026
3a11c71
tui3: teams are read and written through internal/teams
agentfield-bot Sep 24, 2026
810ba39
tui3: every teams edit is the store's read-modify-write
agentfield-bot Sep 24, 2026
d6cf54a
tui3: the manager's place on the strip, the switcher and the wall
agentfield-bot Sep 24, 2026
5fee54b
tui3: the Traffic rail beside the manager, and the manager's stop and…
agentfield-bot Sep 24, 2026
2061864
tui3: test that the Traffic read titles a member that joined untitled
agentfield-bot Sep 24, 2026
e0a922f
manual: the team manager page
agentfield-bot Sep 24, 2026
89f76c3
session: the team manager's half, through internal/teams
agentfield-bot Sep 24, 2026
92ac178
docs: making a manager, its screen, and the design as built
agentfield-bot Sep 24, 2026
edf474d
tui3: the manager's screen, reworked from the UX review
agentfield-bot Sep 24, 2026
6854558
session: team events, the brief as the manager's, and the start's card
agentfield-bot Sep 24, 2026
1436801
teams: read Traffic backwards for a tail and by binary search for a page
agentfield-bot Sep 24, 2026
6f6f721
teams: coarse ages in the digest so its note lands only on a real change
agentfield-bot Sep 24, 2026
bfce415
session: cache member journals by stamp, reuse the teams file, save c…
agentfield-bot Sep 24, 2026
2288c04
tui3, session: the started member wakes once, and the brief is a card
agentfield-bot Sep 24, 2026
2e42d28
docs: the manager's screen as reworked, its keys, and the change note
agentfield-bot Sep 24, 2026
5e49e5b
tui3: the wall reads the front only when it moved, and held tiles on …
agentfield-bot Sep 24, 2026
8d56e8c
tui3: the hint line names a Traffic row's handles whole
agentfield-bot Sep 24, 2026
26ef299
teams: stamps, a compare-and-swap change, and a Traffic reader that s…
agentfield-bot Sep 24, 2026
4f47f3b
remote: the teams doors, answered from the engine's profile
agentfield-bot Sep 24, 2026
ef6323d
tui3: teams and Traffic through a seam, written and read off the loop
agentfield-bot Sep 24, 2026
8f3db6e
codeaf: --host hands the surface the engine's teams
agentfield-bot Sep 24, 2026
ddbcbfb
docs: teams and the manager over --host, against the engine's store
agentfield-bot Sep 24, 2026
25ff869
remote: the wall's team naming and Organize proposals cross the wire
agentfield-bot Sep 24, 2026
2a4746c
tui3: Organize's Undo test compares against the list as written
agentfield-bot Sep 24, 2026
697ae25
tui3: team naming and Organize are asked beside the ordered line, wit…
agentfield-bot Sep 24, 2026
de9af8b
session: a conversation on the ordinary launch finds its teams
agentfield-bot Sep 24, 2026
1bf43fd
session: a manager is told it is one on its first request, as codeaf'…
agentfield-bot Sep 24, 2026
5153049
teams: a derived handle names what the conversation is about
agentfield-bot Sep 24, 2026
2bfb920
manual: the manager is told it is one, and how a handle is made
agentfield-bot Sep 24, 2026
653ca39
teams: a team carries a wake switch, on unless it says wake false
agentfield-bot Sep 24, 2026
dab811e
session: team traffic wakes an idle conversation: a directive its mem…
agentfield-bot Sep 24, 2026
edeea72
engine: open a team conversation nobody holds in its folder's host so…
agentfield-bot Sep 24, 2026
3ee8af9
docs: directives wake members, replies wake the manager, notes wait
agentfield-bot Sep 24, 2026
daedddf
tui3: the strip and the wall show what is open in this window, and of…
agentfield-bot Sep 24, 2026
0216b48
session, teams: a handle is one word the title model chooses
agentfield-bot Sep 24, 2026
d2db4b1
tui3: every team reference in a chat is a door
agentfield-bot Sep 24, 2026
dce2184
docs, manual: the view is what is open here, handles are one chosen w…
agentfield-bot Sep 24, 2026
221baa4
tui3: shorter hints for Open them and a member's link
agentfield-bot Sep 24, 2026
7bd2e4c
tui3: @ completion offers teams and conversations beside files
agentfield-bot Sep 25, 2026
2ae98cc
session: a sent team or chat mention carries a bounded digest
agentfield-bot Sep 25, 2026
e2ddba9
docs, manual: @ names a team or a conversation as well as a file
agentfield-bot Sep 25, 2026
ab104ec
session: team_start honours a team's auto-wake being off
agentfield-bot Sep 25, 2026
0341dd6
session: an asking member goes idle when its process is gone
agentfield-bot Sep 25, 2026
f9f3553
session: a handle ask retries once on a timeout or a dropped connection
agentfield-bot Sep 25, 2026
0776be6
teams: a message to several members is one entry, and an answer names…
agentfield-bot Sep 24, 2026
803b70a
session: a manager's question to several is one entry, and a member's…
agentfield-bot Sep 24, 2026
c83ff2b
tui3: the Traffic rail is threads, the newest at the top
agentfield-bot Sep 24, 2026
f2a2f1f
tui3: a manager's team_send is a thread card, and a member's chat mir…
agentfield-bot Sep 24, 2026
a2f9ccf
tui3: a manager's question is never folded away, and a narrow rail co…
agentfield-bot Sep 24, 2026
91562af
docs, manual: Traffic and threads, what the manager and members see
agentfield-bot Sep 24, 2026
755143c
tui3: the tab strip reads Home, the team chip, the manager, then the …
agentfield-bot Sep 24, 2026
08938b7
tui3: a chats word on the places bar goes back to the conversation
agentfield-bot Sep 25, 2026
ef47a9f
tui3: one top bar, the places bar takes the chat strip's geometry
agentfield-bot Sep 25, 2026
3287f0a
tui3: with the manager in front the right column is the traffic
agentfield-bot Sep 25, 2026
3f0c9a1
tui3: a handle on the Traffic opens the member at the message
agentfield-bot Sep 25, 2026
3bba987
tui3: the manager's tasks header says its way back in the hint line
agentfield-bot Sep 25, 2026
bcbf1a0
manual, tui3: no em dash on the lines the place renumbering touched
agentfield-bot Sep 25, 2026
788d2f3
session: the wake tests read the delivered brief, not its wording
agentfield-bot Sep 25, 2026
3454b5b
codeaf: the engine wake tests look for the directive, not its wording
agentfield-bot Sep 25, 2026
101e60f
tui3: the dock under the box says chats, and a hover says where a pre…
agentfield-bot Sep 25, 2026
c6fabef
Merge origin/dev into feat/conversation-overview
agentfield-bot Sep 25, 2026
d6d896b
tui3: the teams surfaces ask the glyph door for their marks
agentfield-bot Sep 25, 2026
1aa24a5
tui3: the home strip tests read the one top bar's lead
agentfield-bot Sep 25, 2026
6416a6c
tui3: the one top bar test finds the strip's home by dev's lowercase …
agentfield-bot Sep 25, 2026
6d297d0
remote: the engine's teams read sets an unreadable teams.json aside
AbirAbbas Sep 25, 2026
f26f25c
session: team_stop says a background-opened member is not stopped
AbirAbbas Sep 25, 2026
211f1d6
manual: the conversations view answers to the wall
AbirAbbas Sep 25, 2026
1abf425
Merge remote-tracking branch 'origin/dev' into review/sp-1429-dev
AbirAbbas Sep 25, 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
17 changes: 17 additions & 0 deletions cmd/codeaf/chatv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1979,25 +1979,40 @@ func v3PolicyMode(workspace, profileDir, mode string) (*approval.Policy, error)
// - Pure reads of this machine — read, grep, find, ls. Nothing here changes a
// file, so the blanket mode's prompt is free to land where it matters, on
// bash, edit and write.
//
// - jobs, whose list and output are reads of processes the person already
// started. Its kill is not on the floor: it inherits the blanket mode,
// which asks.
//
// - The agent's own bookkeeping — remember, track and recall. These write to
// and read from the memories and working state it keeps for itself
// (internal/session's memory.go and state.go); no hand outside this process
// reads them, and asking somebody to approve the agent writing itself a
// reminder is asking about the wrong thing.
//
// - manual, which reads pages compiled into this binary and touches no disk
// at all (internal/session's tools_manual.go). A person who asks "what can
// you do" and is answered with a permission prompt has been asked to
// approve the program looking up its own documentation.
//
// - settings, which reads the person's own settings rows back through the
// registry (internal/session's tools_settings.go). It is manual's shape one
// file over — the answer to "what is my daily budget" is a lookup, and the
// credential rows read MASKED through the registry itself
// ([config.Setting.Secret]), so there is nothing here a prompt would be
// protecting.
//
// - The team verbs that stay inside a team the person made
// (internal/session's tools_team.go): a manager reading its members'
// states and pages, sending them a line, ending a member's turn the way
// the person's own Stop does, and a member posting to its room. Every one
// of them is a line in the team's own traffic log, which the person watches
// on the manager's rail, and none of them reaches a permission prompt.
//
// team_start is DELIBERATELY NOT HERE. It opens a new conversation that spends
// money for as long as it runs, so the blanket mode asks, the way it asks about
// propose_task.
//
// commit is DELIBERATELY NOT HERE, and it is the interesting half of the split.
// It is the fifth hand on the same working state, but it is the only one that
// declares a tracked subgoal FINISHED, and a session that can mark its own work
Expand All @@ -2020,6 +2035,8 @@ func v3BuiltinApprovals() map[string]any {
"jobs": "allow",
"remember": "allow", "track": "allow", "recall": "allow",
"manual": "allow", "settings": "allow",
"team_status": "allow", "team_read": "allow", "team_send": "allow",
"team_stop": "allow", "team_post": "allow",
}
}

Expand Down
15 changes: 15 additions & 0 deletions cmd/codeaf/chatv3_approvalfloor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,18 @@ func TestTheFloorDoesNotWidenAnySafetyFloor(t *testing.T) {
// And a bash call whose command cannot be read degrades rather than runs.
wantAction(t, policy, "bash", `{}`, approval.ActionPrompt)
}

// THE TEAM VERBS SPLIT READ AND MESSAGE FROM START (internal/session's
// tools_team.go). A manager looking at its team, sending it a line, ending a
// member's turn and a member posting to its room stay inside the team the person
// made and are logged in its traffic, so they sit on the floor; a new member is a
// new conversation that spends money, so team_start is the blanket mode's and
// asks under the shipped default.
func TestTheTeamVerbsAreAllowedAndTeamStartAsks(t *testing.T) {
dir := v3Profile(t, map[string]any{"tools.approvalMode": "prompt"})
policy := gateOf(t, dir)
for _, tool := range []string{"team_status", "team_read", "team_send", "team_stop", "team_post"} {
wantAction(t, policy, tool, `{"to":"web","text":"x","handle":"web"}`, approval.ActionAllow)
}
wantAction(t, policy, "team_start", `{"handle":"docs","brief":"write the README"}`, approval.ActionPrompt)
}
6 changes: 6 additions & 0 deletions cmd/codeaf/chatv3_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,12 @@ func hostOptions(fleet *engineFleet, welcome remote.Welcome, pick bool) (tui3.Op
// which is not an optimization but the seam's stated law, because that
// segment is asked on the frame. Save travels synchronously: it is a
// keystroke, it is rare, and somebody is waiting for its answer.
// THE TEAMS, AS THE ENGINE MACHINE KEEPS THEM. The far session's team
// tools write the teams file and each team's Traffic into the engine's
// profile, so the window reads and writes those, over the wire
// ([hostTeams]); an engine without the doors hands no seam and the
// surface turns teams off rather than keeping them on this laptop.
Teams: hostTeamsSeam(far, welcome),
Standing: tui3.StandingSeam{
Items: stands.list,
Save: stands.save,
Expand Down
180 changes: 180 additions & 0 deletions cmd/codeaf/chatv3_host_teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package main

import (
"errors"
"sync"

"github.com/Agent-Field/codeaf/internal/remote"
teamstore "github.com/Agent-Field/codeaf/internal/teams"
"github.com/Agent-Field/codeaf/internal/tui3"
)

// hostTeams is [tui3.TeamsSeam] over the wire: the ENGINE machine's teams file
// and Traffic logs, which are where the far session's team tools keep them
// (internal/remote's wire_teams.go).
//
// IT IS BUILT LIKE [hostStanding], for the one call the surface makes on its
// loop. [tui3.TeamsSeam.Load] is asked at an opening and must not block, so it
// answers what is held and nothing else; when nothing is held yet it says so,
// and the surface asks [tui3.TeamsSeam.ReadSince] off its loop, once, which
// fills what is held. Every other door here is called from a command and may
// take a round trip: the Traffic clock's turn, and the write an edit queued.
// There is no clock of its own: the surface's Traffic clock is the only thing
// that asks again, and it runs only while a managed team is held.
//
// NOTHING HERE READS THIS LAPTOP'S PROFILE. The teams a window over --host
// draws are the far machine's, or none: an engine without the doors gets no
// seam at all ([hostTeamsSeam]), and the surface turns teams off rather than
// falling back to the file on this disk, which the far session never reads.
type hostTeams struct {
// read, write and traffic are the three wire doors, as closures for
// [hostStanding]'s reason: a test hands them a conflict without a pipe.
read func(stamp string, reserved []float64) (remote.TeamsReading, error)
write func(base string, teams []teamstore.Team) (remote.TeamsReading, error)
traffic func(team, after string, limit int) (remote.TeamsTraffic, error)

mu sync.Mutex
// teams and stamp are the last list the engine answered with and the
// file's stamp it was at; known says there has been one.
teams []teamstore.Team
stamp string
known bool
// reserved is the palette's reserved hues as the surface last passed
// them, for a read this seam makes on its own.
reserved []float64
}

// hostTeamsTries is how many times a write that met another writer is made
// again from a fresh read before it gives up and says so.
const hostTeamsTries = 3

// errHostTeamsBusy is a write that met another writer on every try.
var errHostTeamsBusy = errors.New("the teams on the far machine kept changing while this was written; try again")

func newHostTeams(far hostFar) *hostTeams {
return &hostTeams{
read: far.client.TeamsRead,
write: far.client.TeamsUpdate,
traffic: far.client.TeamsTraffic,
}
}

// hostTeamsSeam is the seam the --host door hands the surface: the far
// machine's teams when the engine answers the teams doors, and the zero seam
// when it does not or there is no connection. The zero seam over --host is
// teams off (internal/tui3's [app.teamsOff]), never this laptop's file.
func hostTeamsSeam(far hostFar, welcome remote.Welcome) tui3.TeamsSeam {
if far.client == nil || !welcome.Teams {
return tui3.TeamsSeam{}
}
return newHostTeams(far).seam()
}

// seam is h as the surface's functions.
func (h *hostTeams) seam() tui3.TeamsSeam {
return tui3.TeamsSeam{Load: h.load, ReadSince: h.readSince, Update: h.update, Traffic: h.readTraffic}
}

// load is [tui3.TeamsSeam.Load]: what is held, now, and never the wire.
func (h *hostTeams) load(reserved []float64) ([]teamstore.Team, string, bool) {
return h.held(reserved)
}

// held is the list held, a copy, and remembers the reserved hues asked with.
func (h *hostTeams) held(reserved []float64) ([]teamstore.Team, string, bool) {
h.mu.Lock()
defer h.mu.Unlock()
if reserved != nil {
h.reserved = append([]float64(nil), reserved...)
}
return cloneTeams(h.teams), h.stamp, h.known
}

// keep holds a list the engine answered with.
func (h *hostTeams) keep(teams []teamstore.Team, stamp string) {
h.mu.Lock()
defer h.mu.Unlock()
h.teams, h.stamp, h.known = cloneTeams(teams), stamp, true
}

// readSince is [tui3.TeamsSeam.ReadSince]: one round trip, whose answer is a
// few bytes when the file is still at since.
func (h *hostTeams) readSince(since string, reserved []float64) ([]teamstore.Team, string, bool, error) {
reading, err := h.read(since, reserved)
if err != nil {
return nil, "", false, err
}
if reading.Same {
return nil, reading.Stamp, true, nil
}
h.keep(reading.Teams, reading.Stamp)
return reading.Teams, reading.Stamp, false, nil
}

// update is [tui3.TeamsSeam.Update] as a compare-and-swap over the wire.
//
// THE LOCK CANNOT CROSS THE WIRE, SO THE WRITE IS CONDITIONAL. The change is
// made to the list held (or read now, when none is), and the whole list goes
// back with the stamp it was made from; the engine writes it only if its file
// is still at that stamp (internal/teams' ChangeIf). A file that moved in
// between (the far session made a manager, set a handle, started a member)
// answers Stale, and the list is read again and the change made again on top
// of it, up to [hostTeamsTries] times. So what another writer did is kept, as
// the local store's read-modify-write keeps it, and nothing is ever written
// over a list this window did not see.
func (h *hostTeams) update(change func(*teamstore.File) error) ([]teamstore.Team, string, error) {
teams, stamp, known := h.held(nil)
for try := 0; try < hostTeamsTries; try++ {
if !known || try > 0 {
reserved := h.reservedHues()
fresh, at, _, err := h.readSince("", reserved)
if err != nil {
return nil, "", err
}
teams, stamp = fresh, at
}
f := &teamstore.File{Version: teamstore.Version, Teams: cloneTeams(teams)}
if err := change(f); err != nil {
return nil, "", err
}
reply, err := h.write(stamp, f.Teams)
if err != nil {
return nil, "", err
}
if reply.Stale {
continue
}
h.keep(reply.Teams, reply.Stamp)
return reply.Teams, reply.Stamp, nil
}
return nil, "", errHostTeamsBusy
}

// reservedHues is the palette's reserved hues as last passed.
func (h *hostTeams) reservedHues() []float64 {
h.mu.Lock()
defer h.mu.Unlock()
return append([]float64(nil), h.reserved...)
}

// readTraffic is [tui3.TeamsSeam.Traffic]: one round trip, answered from a
// stat on the engine when the log has not moved.
func (h *hostTeams) readTraffic(team, after string, limit int) ([]teamstore.Entry, error) {
got, err := h.traffic(team, after, limit)
if err != nil {
return nil, err
}
return got.Entries, nil
}

// cloneTeams is a copy of teams that shares nothing with it.
func cloneTeams(teams []teamstore.Team) []teamstore.Team {
if teams == nil {
return nil
}
out := make([]teamstore.Team, len(teams))
for i, t := range teams {
out[i] = t.Clone()
}
return out
}
107 changes: 107 additions & 0 deletions cmd/codeaf/chatv3_host_teams_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package main

import (
"errors"
"testing"

"github.com/Agent-Field/codeaf/internal/remote"
teamstore "github.com/Agent-Field/codeaf/internal/teams"
)

// A WRITE THAT MEETS ANOTHER WRITER IS MADE AGAIN ON TOP OF IT. The window made
// its change to the list it held; the far session set a manager in between, so
// the engine answers Stale. The seam reads the file again, makes the change
// again on the fresh list, and the second write carries both: the rename and
// the manager nobody here saw.
func TestHostTeamsRetriesAStaleWriteOnTopOfTheOtherWriter(t *testing.T) {
far := []teamstore.Team{{ID: "0a0a0a0a0a0a", Name: "harbor", Members: []teamstore.Member{{Key: "k1"}}}}
stamp, reads := "1.1", 0
var wrote [][]teamstore.Team
h := &hostTeams{
read: func(since string, _ []float64) (remote.TeamsReading, error) {
reads++
if since == stamp {
return remote.TeamsReading{Stamp: stamp, Same: true}, nil
}
return remote.TeamsReading{Stamp: stamp, Teams: cloneTeams(far)}, nil
},
write: func(base string, teams []teamstore.Team) (remote.TeamsReading, error) {
wrote = append(wrote, cloneTeams(teams))
if base != stamp {
return remote.TeamsReading{Stamp: stamp, Stale: true}, nil
}
far, stamp = cloneTeams(teams), "2.2"
return remote.TeamsReading{Stamp: stamp, Teams: cloneTeams(far)}, nil
},
}
if _, _, known := h.load(nil); known {
t.Fatal("a seam that has read nothing says it holds something")
}
if _, _, _, err := h.readSince("", nil); err != nil {
t.Fatal(err)
}
// The far session writes after the window read.
far[0].Manager, stamp = "k1", "1.5"

calls := 0
teams, at, err := h.update(func(f *teamstore.File) error {
calls++
f.Teams[0].Name = "dock"
return nil
})
if err != nil {
t.Fatal(err)
}
if calls != 2 || len(wrote) != 2 || reads != 2 {
t.Fatalf("the change was made %d times, written %d times, read %d times", calls, len(wrote), reads)
}
if at != "2.2" || teams[0].Name != "dock" || teams[0].Manager != "k1" {
t.Fatalf("the second write lost something: %+v at %q", teams, at)
}
if held, heldAt, known := h.load(nil); !known || heldAt != "2.2" || held[0].Manager != "k1" {
t.Fatalf("the seam holds %+v at %q", held, heldAt)
}
}

// A WRITER THAT NEVER STOPS WINS, AND THE WINDOW IS TOLD. Three stale answers
// in a row are an error the surface says, not a write made over the top.
func TestHostTeamsGivesUpAfterThreeStaleWrites(t *testing.T) {
writes := 0
h := &hostTeams{
read: func(string, []float64) (remote.TeamsReading, error) {
return remote.TeamsReading{Stamp: "1.1"}, nil
},
write: func(string, []teamstore.Team) (remote.TeamsReading, error) {
writes++
return remote.TeamsReading{Stamp: "9.9", Stale: true}, nil
},
}
if _, _, err := h.update(func(*teamstore.File) error { return nil }); !errors.Is(err, errHostTeamsBusy) || writes != hostTeamsTries {
t.Fatalf("after %d stale writes: %v", writes, err)
}
}

// AN ENGINE WITHOUT THE TEAMS DOORS GETS NO SEAM. The welcome of an older
// engine carries no Teams, and the door hands the surface the zero seam, which
// over --host is teams off; it never hands one onto this laptop's profile.
func TestAnOlderEngineGetsNoTeamsSeam(t *testing.T) {
loop, err := remote.Loopback(remote.Hello{Version: remote.Version}, remote.Options{Boot: func(remote.Hello) (*remote.Engine, error) {
return &remote.Engine{Agent: &quietAgent{}, ProfileDir: t.TempDir()}, nil
}})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = loop.Close() })
far := hostFar{client: loop.Client}
welcome := loop.Client.Welcome()
if seam := hostTeamsSeam(far, welcome); seam.Load == nil || seam.Update == nil || seam.Traffic == nil || seam.ReadSince == nil {
t.Fatal("an engine with the teams doors got no seam")
}
welcome.Teams = false
if seam := hostTeamsSeam(far, welcome); seam.Load != nil || seam.Update != nil {
t.Fatal("an older engine got a teams seam")
}
if seam := hostTeamsSeam(hostFar{}, remote.Welcome{Teams: true}); seam.Load != nil {
t.Fatal("no connection got a teams seam")
}
}
4 changes: 4 additions & 0 deletions cmd/codeaf/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ func runRemoteEngine(args []string) error {
return fmt.Errorf("usage: codeaf engine [--workspace path] [--session path] [--no-host] [--status] [--status-all] [--stop] [--stop-all]")
}

// AN ENGINE CAN OPEN A TEAM CONVERSATION NOBODY HOLDS, so team traffic can
// wake it (team_resume.go). Only the two roads below that serve sessions
// ever use the door; the splice between them serves none.
armTeamResume()
if *daemon {
return runEngineHost(*workspace, *file)
}
Expand Down
Loading
Loading