Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dbc39e7
taskdoor: a door paired computers reach each other's tasks through
AbirAbbas Oct 30, 2026
7372fe9
taskdoor: send a task copy out as a delta and bring its change home
AbirAbbas Oct 30, 2026
e0e2108
serve: take tasks from paired computers
AbirAbbas Oct 30, 2026
6ead597
config: remember which paired computer tasks run on
AbirAbbas Oct 30, 2026
f49903b
session: run a task on a paired computer
AbirAbbas Oct 30, 2026
91e81a6
taskplace: follow remote tasks and bring their changes home
AbirAbbas Oct 30, 2026
6d5488d
chat: wire paired task computers into v3 conversations
AbirAbbas Oct 30, 2026
df0caf0
session: carry effort and the card's computer choice through admission
AbirAbbas Oct 30, 2026
a89a467
remote: forward task placement to a window on this computer's engine
AbirAbbas Oct 30, 2026
8c294bc
tui3: say where a task runs, choose the computer, and run it here
AbirAbbas Oct 30, 2026
803efca
chat: find paired task computers on an ordinary launch and a connecte…
AbirAbbas Oct 31, 2026
9e8d7ba
e2e: a task runs on the other home and lands here
AbirAbbas Oct 31, 2026
abfe77d
manual: tasks on another computer
AbirAbbas Oct 31, 2026
a3291b3
tasks: nothing about other computers until a person chooses one
AbirAbbas Oct 31, 2026
8b47c06
serve: take tasks only with --take-tasks, and leave pairing's code alone
AbirAbbas Oct 31, 2026
90d075a
run: order steps across tasks only inside the task door's child
AbirAbbas Oct 31, 2026
dcb0ca9
manual: tasks go elsewhere only by choice, and serve takes them with …
AbirAbbas Oct 31, 2026
5055d93
devbox: take tasks when the box's installed build can
AbirAbbas Oct 31, 2026
cba3ad6
serve: name --take-tasks in the usage lines
AbirAbbas Oct 31, 2026
95b0172
manual: a remote task's private apply has the same Mac limit as a loc…
AbirAbbas Oct 31, 2026
395d01c
changes: a task can run on another paired computer
AbirAbbas Oct 31, 2026
3c53260
fix: hide Run tasks on from the settings tool without task computers
AbirAbbas Oct 31, 2026
f0ce8bb
fix: refuse a finished attempt started again and a removed computer o…
AbirAbbas Oct 31, 2026
40b05fb
fix: keep an edit made while a result installs, and leave stop free d…
AbirAbbas Oct 31, 2026
32ea83f
fix: keep mirrored record kinds, find an owed acknowledgement on reop…
AbirAbbas Oct 31, 2026
af2fc9e
docs: quote every task-transfer refusal in the chat manual, with what…
AbirAbbas Oct 31, 2026
4a870ec
fix: meet the settings, chord, off-loop, guarded-goroutine and comple…
AbirAbbas Oct 31, 2026
b3c5154
fix: a task sent to another computer answers before its copy is prepared
AbirAbbas Oct 31, 2026
d524a66
docs: say a task sent elsewhere appears before its copy is ready
AbirAbbas Oct 31, 2026
a697e67
fix: every list that shows a task sent elsewhere says where it ran
AbirAbbas Oct 31, 2026
3987b53
fix: a task child sends no reasoning effort to a model without reasoning
AbirAbbas Oct 31, 2026
a6e79d7
fix: the task rail and /history rows say which computer a task ran on
AbirAbbas Oct 31, 2026
7aed329
fix: a task child retries once without effort when the model cannot t…
AbirAbbas Oct 31, 2026
4c4d05e
docs: the chats-stored heading carries the words people ask with
AbirAbbas Oct 31, 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
7 changes: 6 additions & 1 deletion cmd/codeaf/chatv3_at.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (

"github.com/Agent-Field/codeaf/internal/pair"
"github.com/Agent-Field/codeaf/internal/remote"
"github.com/Agent-Field/codeaf/internal/taskdoor"
codeupdate "github.com/Agent-Field/codeaf/internal/update"
)

Expand Down Expand Up @@ -261,14 +262,15 @@ func askPairingCode(name string) (string, error) {
// walks out to the relay and stays there.
func runServe(args []string) error {
flags := commandFlags("serve")
takeTasks := flags.Bool(taskdoor.TakeTasksFlag, false, "take tasks from your paired computers")
workspace := flags.String("workspace", "", "directory a connection works in when it does not name one; empty is the directory this command was run in")
relayAddress := flags.String("via", "", "the sync address to be reachable through; empty reads "+pair.RelayEnv+", ~/.codeaf/v3/remote/relay, then the address this machine syncs through")
renamedFlag(flags, "relay", "via")
if err := parseCommandFlags(flags, args); err != nil {
return err
}
if flags.NArg() != 0 {
return errors.New("usage: codeaf serve [--workspace path] [--via https://…]")
return errors.New("usage: codeaf serve [--workspace path] [--via https://…] [--" + taskdoor.TakeTasksFlag + "]")
}

service := strings.TrimSpace(*relayAddress)
Expand Down Expand Up @@ -300,6 +302,9 @@ func runServe(args []string) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
defer startServeKeeping(ctx, service)()
if *takeTasks {
defer serveTasks(ctx, service)()
}
host := &pair.Host{
Service: service,
Device: device,
Expand Down
5 changes: 5 additions & 0 deletions cmd/codeaf/chatv3_lanes.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ func v3LanesOverWire() v3Lanes {
// are one fact. A door that took the lanes and then picked its own builder could
// turn the verb on with nothing behind it.
func v3Shape(cfg session.Config, lanes v3Lanes) (session.Config, func(session.Config) (*session.Agent, error)) {
// Task placement works across the connection independently of adaptive
// run lanes, so disabling those lanes must not hide paired computers.
if cfg.TaskComputers == nil {
cfg.TaskComputers = v3TaskComputers(cfg.ProfileDir)
}
if !lanes.Designs {
// Nil is the honest way to say the designer is not among the things this
// conversation can do, so the model says as much instead of starting two
Expand Down
3 changes: 3 additions & 0 deletions cmd/codeaf/chatv3_orchestrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func v3OpenSession(cfg session.Config) (*session.Agent, error) {
// Every conversation this process opens counts its task lanes in one
// account, because they are all running on one machine.
cfg.TaskLanes = v3MachineLanes
if cfg.TaskComputers == nil {
cfg.TaskComputers = v3TaskComputers(cfg.ProfileDir)
}
cfg, runs := v3Adaptive(cfg)
agent, err := session.New(cfg)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions cmd/codeaf/chatv3_surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func runSurface(ctx context.Context, options tui3.Options) error {
options.Restart = restart
}
options.CellsUnavailable = cell.UnavailableReason()
if options.TaskComputers == nil && options.Host == "" {
options.TaskComputers = v3TaskComputers(options.ProfileDir)
}
if options.CellsUnavailable == "" && options.Host == "" {
wireSync(&options)
options.WaitForRelease = func() bool {
Expand Down
53 changes: 53 additions & 0 deletions cmd/codeaf/chatv3_taskcomputers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"net/http"
"path/filepath"
"runtime"
"time"

"github.com/Agent-Field/codeaf/internal/blobstore"
"github.com/Agent-Field/codeaf/internal/cellstore"
"github.com/Agent-Field/codeaf/internal/directory"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/identity"
"github.com/Agent-Field/codeaf/internal/reqsign"
"github.com/Agent-Field/codeaf/internal/session"
"github.com/Agent-Field/codeaf/internal/syncsetup"
"github.com/Agent-Field/codeaf/internal/taskdoor"
"github.com/Agent-Field/codeaf/internal/taskplace"
)

// v3TaskComputers builds no client while this home is quiet, unpaired or on an
// unsupported platform. A nil door also keeps the proposal argument absent.
func v3TaskComputers(profile string) session.TaskComputers {
// Identity storage takes a concrete home, while an unset profile means
// the ordinary state root to the rest of the conversation configuration.
if profile == "" {
profile = home.Dir()
}
if runtime.GOOS == "windows" || syncsetup.Quiet(profile) {
return nil
}
route := syncsetup.Resolve(profile)
if !route.On() {
return nil
}
id, err := identity.Load(profile)
if err != nil {
return nil
}
device, err := identity.LoadDevice(profile)
if err != nil || device.Cert.Verify(id.PublicKey()) != nil {
return nil
}
// This factory is called for every conversation. Only existing keys are
// read here; directory requests belong to an explicit placement choice.
client := directory.NewHTTP(route.URL, reqsign.SignFor(homeSigner{id, device}, time.Now), &http.Client{Timeout: linkTimeout})
engine := cellstore.EngineFor("")
engine.DataRoot = filepath.Join(profile, "v3", "stores")
engine.Guard.Home = profile
store := blobstore.NewHTTP(route.URL, reqsign.SignFor(homeSigner{id, device}, time.Now), &http.Client{Timeout: time.Minute})
copies := taskdoor.CopyDeps{Root: filepath.Join(profile, "v3", "taskcopies"), Service: route.URL, Identity: id, Device: device, Engine: engine, Store: store}
return &taskplace.Computers{Directory: client, Copies: copies, Home: profile}
}
55 changes: 55 additions & 0 deletions cmd/codeaf/chatv3_taskcomputers_default_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"net/http"
"net/http/httptest"
"runtime"
"testing"

"github.com/Agent-Field/codeaf/internal/devname"
"github.com/Agent-Field/codeaf/internal/identity"
"github.com/Agent-Field/codeaf/internal/session"
)

// DESIGN 1 requires the ordinary chat launch to find the paired identity in
// the same home that serve uses, without a separate profile override.
func TestConversationTaskComputersUseTheDefaultHomeWhenTheProfileIsUnset(t *testing.T) {
_ = pairedTaskDefaultHome(t)
computers := v3TaskComputers("")
if computers == nil || computers.ThisComputer() != "chat computer" {
t.Fatal("the normal profile hides paired task computers")
}
}

// DESIGN 1 also applies over the ordinary engine connection; disabling an
// unrelated run lane must not remove the task placement that /task --on uses.
func TestConnectedConversationsKeepPairedTaskPlacement(t *testing.T) {
profile := pairedTaskDefaultHome(t)
cfg, _ := v3Shape(session.Config{ProfileDir: profile}, v3LanesOverWire())
if cfg.TaskComputers == nil || cfg.TaskComputers.ThisComputer() != "chat computer" {
t.Fatal("the connected conversation hides paired task placement")
}
}

func pairedTaskDefaultHome(t *testing.T) string {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("paired tasks require Linux or macOS")
}
profile := t.TempDir()
t.Setenv("HOME", t.TempDir())
t.Setenv("CODEAF_HOME", profile)
t.Setenv("CODEAF_PROFILE_DIR", "")
t.Setenv("CODEAF_TELEMETRY", "off")
t.Setenv("DO_NOT_TRACK", "1")
server := httptest.NewServer(http.NotFoundHandler())
t.Cleanup(server.Close)
t.Setenv("CODEAF_SYNC_URL", server.URL)
if _, err := identity.Ensure(profile); err != nil {
t.Fatal(err)
}
if _, err := devname.Set(profile, "chat computer"); err != nil {
t.Fatal(err)
}
return profile
}
115 changes: 115 additions & 0 deletions cmd/codeaf/chatv3_taskcomputers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"context"
"net/http/httptest"
"runtime"
"testing"
"time"

"github.com/Agent-Field/codeaf/internal/directory"
"github.com/Agent-Field/codeaf/internal/identity"
"github.com/Agent-Field/codeaf/internal/reqsign"
)

// The absence law is applied at the actual conversation configuration door.
func TestConversationTaskComputersAreAbsentInUnpairedSoloAndSyncOffHomes(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv("CODEAF_TELEMETRY", "off")
t.Setenv("DO_NOT_TRACK", "1")
for _, kind := range []string{"unpaired", "solo", "off"} {
t.Run(kind, func(t *testing.T) {
profile := t.TempDir()
if kind == "solo" {
if _, err := identity.EnsureSolo(profile); err != nil {
t.Fatal(err)
}
}
if kind == "off" {
if _, err := identity.Ensure(profile); err != nil {
t.Fatal(err)
}
t.Setenv("CODEAF_SYNC_URL", "off")
}
if got := v3TaskComputers(profile); got != nil {
t.Fatalf("%s offered task computers", kind)
}
})
}
}

// Contract 6 selects from the signed pairing roster, including unsupported
// Windows rows and excluding this computer and revoked device certificates.
func TestConversationTaskComputersUsePairingNamesAndRevocation(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv("CODEAF_TELEMETRY", "off")
t.Setenv("DO_NOT_TRACK", "1")
profile := t.TempDir()
id, err := identity.Ensure(profile)
if err != nil {
t.Fatal(err)
}
self, err := identity.LoadDevice(profile)
if err != nil {
t.Fatal(err)
}
state := directory.NewMemory(time.Now)
client := state.For(self.ID())
key := directory.MetadataKey(id.CellKey())
var revoked string
for _, item := range []struct {
name, platform string
revoke bool
}{{"chat computer", "linux", false}, {"work computer", "darwin", false}, {"Windows computer", "windows", false}, {"removed computer", "linux", true}} {
dev := self
if item.name != "chat computer" {
dev, err = identity.NewDevice(id)
if err != nil {
t.Fatal(err)
}
}
name, err := directory.SealName(key, item.name)
if err != nil {
t.Fatal(err)
}
if err := client.PutDevice(context.Background(), dev.ID(), directory.Device{V: 1, Name: name, Platform: item.platform}); err != nil {
t.Fatal(err)
}
if item.revoke {
revoked = dev.ID()
if err := client.Revoke(context.Background(), dev.ID()); err != nil {
t.Fatal(err)
}
}
}
server := httptest.NewServer(directory.Handler(reqsign.AuthenticateAt(time.Now), func(string) (directory.Directory, error) { return state, nil }))
defer server.Close()
t.Setenv("CODEAF_SYNC_URL", server.URL)
computers := v3TaskComputers(profile)
if runtime.GOOS == "windows" {
if computers != nil {
t.Fatal("Windows origin offered remote tasks")
}
return
}
if computers == nil {
t.Fatal("paired home has no task computers")
}
rows, err := computers.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(rows) != 2 {
t.Fatalf("paired computers = %+v", rows)
}
for _, row := range rows {
if row.Device == self.ID() || row.Device == revoked {
t.Fatalf("non-target listed: %+v", row)
}
if row.Name == "Windows computer" && row.CanTakeTasks {
t.Fatal("Windows target can take tasks")
}
}
}
Loading
Loading