Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7ad00ab
docs: describe workspace storage and relay architecture
santoshkumarradha Oct 25, 2026
158eb5e
docs: record architecture-only change for PR 1944
santoshkumarradha Oct 25, 2026
8c573f6
feat: own task layouts and transfer multi-root chats safely
santoshkumarradha Oct 25, 2026
8f055d5
fix: register workspace runtime and prototype environment plumbing
santoshkumarradha Oct 25, 2026
5856fea
feat: review and apply task snapshots without losing private copies
santoshkumarradha Oct 25, 2026
faa7efb
refactor: separate task snapshot validation and persistence phases
santoshkumarradha Oct 25, 2026
181f1b6
merge: integrate current project knowledge and documentation layout
santoshkumarradha Oct 25, 2026
7a84d42
Pin published Furrow v0.3.1 native artifacts
santoshkumarradha Oct 25, 2026
0eb0c3c
Preserve portable workspace state with recoverable explicit apply
santoshkumarradha Oct 26, 2026
e5789c8
Explain portable apply transactions and compatibility limits
santoshkumarradha Oct 26, 2026
134c5fd
Keep native workspace metadata outside portable project state
santoshkumarradha Oct 26, 2026
0e4ed86
Clarify native control metadata ownership
santoshkumarradha Oct 26, 2026
31a3359
Persist review workspace identity without prototype overrides
santoshkumarradha Oct 26, 2026
14d225c
Isolate memory close test from background route networking
santoshkumarradha Oct 26, 2026
287ca59
Route folder picker paste without changing the chat draft
santoshkumarradha Oct 26, 2026
661605b
Allow explicitly reviewed apply on the chosen branch
santoshkumarradha Oct 26, 2026
69af064
Join stopped-result fixture work before checking new runs
santoshkumarradha Oct 26, 2026
fbeedde
Document explicit apply consent and automatic branch protection
santoshkumarradha Oct 26, 2026
df95fac
Pin released non-mutating SQLite capture engine
santoshkumarradha Oct 26, 2026
3ea4f74
Document non-mutating database capture contract
santoshkumarradha Oct 26, 2026
0599766
Keep typed folder targets through asynchronous listings
santoshkumarradha Oct 26, 2026
c3f986a
Document safe asynchronous receiver folder selection
santoshkumarradha Oct 26, 2026
a4e25d6
Resolve plan worker transcripts from their owned local history
santoshkumarradha Oct 26, 2026
c543991
Document local task history and adopted checkpoint semantics
santoshkumarradha Oct 26, 2026
6989f72
Recover recorded worker model on plan task pages
santoshkumarradha Oct 26, 2026
b5abc5a
Merge remote-tracking branch 'origin/dev' into HEAD
santoshkumarradha Oct 26, 2026
1f088f2
Stop owned sealing test daemon before temporary store cleanup
santoshkumarradha Oct 26, 2026
2234733
Restore recorded models before replaying transferred task cards
santoshkumarradha Oct 26, 2026
d689b16
Pin local task directory before reading fallback worker headers
santoshkumarradha Oct 26, 2026
b1d3caf
docs: clarify workspace and relay agent contract
santoshkumarradha Oct 26, 2026
ca7ede1
Merge remote-tracking branch 'origin/dev' into sync/workspace-relay-d…
santoshkumarradha Oct 26, 2026
88729bb
Merge origin/dev into #1944: the Raft relay, two-computer fixes and f…
AbirAbbas Oct 26, 2026
dfefbe3
fix(chat): keep automatic task landing unless private copies are opte…
AbirAbbas Oct 26, 2026
369b31c
fix(tasks): refuse Apply across profile and project disks before any …
AbirAbbas Oct 26, 2026
3a7b0cb
docs(tasks): scope private workspaces to the opt-in experiment
AbirAbbas Oct 26, 2026
5942519
fix(two computers): a removed computer keeps working on its own chats
AbirAbbas Oct 26, 2026
cb0fedd
refactor(tasks): split Apply's door and applicability checks out of A…
AbirAbbas Oct 26, 2026
152c2d8
fix(two computers): Home says the whole sentence when a move fails un…
AbirAbbas Oct 26, 2026
364f30d
fix(chat): one activity row per chat, and moved task journals reopen
AbirAbbas Oct 26, 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
8 changes: 4 additions & 4 deletions cmd/codeaf/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// that empties it.
//
// `codeaf cache` answers the question and changes nothing. `codeaf cache clean`
// deletes ~/.codeaf/cache — the shared toolchain caches every task worker fills
// deletes the displayed cache locations — the shared caches task workers fill
// (internal/cachedir says what lives there and what never does) — and because a
// deletion cannot be undone it is guarded the way destructive command lines are
// guarded everywhere a person has already learned: the blast radius is printed
Expand Down Expand Up @@ -62,12 +62,12 @@ func runCacheWith(args []string, input io.Reader, output io.Writer) error {
func showCache(output io.Writer) error {
size := cachedir.Size()
if size == 0 {
_, err := fmt.Fprintf(output, "the cache is empty · %s\n", cachedir.Root())
_, err := fmt.Fprintf(output, "the cache is empty · %s\n", strings.Join(cachedir.Locations(), ", "))
return err
}
_, err := fmt.Fprintf(output,
"the cache holds %s · %s\nshared toolchain caches — go modules, builds, npm, pip, cargo. `codeaf cache clean` frees it.\n",
cachedir.Human(size), cachedir.Root())
cachedir.Human(size), strings.Join(cachedir.Locations(), ", "))
return err
}

Expand Down Expand Up @@ -96,7 +96,7 @@ func cleanCache(args []string, input io.Reader, output io.Writer) error {
// stdout means `codeaf cache clean | tee clean.log` puts the question
// into the file and leaves the person looking at a blank terminal,
// waiting for a word they cannot see. See streams.go.
fmt.Fprintf(aside, "This deletes the shared build cache — %s at %s.\n", cachedir.Human(size), cachedir.Root())
fmt.Fprintf(aside, "This deletes the shared build cache — %s at %s.\n", cachedir.Human(size), strings.Join(cachedir.Locations(), ", "))
fmt.Fprintln(aside, "Toolchains and modules are re-downloaded cold on the next task. Conversations,")
fmt.Fprintln(aside, "settings and credentials live elsewhere and are not touched.")
fmt.Fprint(aside, `Type "`+cacheCleanWord+`" to delete it; anything else keeps it: `)
Expand Down
29 changes: 29 additions & 0 deletions cmd/codeaf/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,32 @@ func TestBothSurfacesConfirmTheCacheDeletionWithTheSameWord(t *testing.T) {
t.Fatalf("a word that is not %q did not keep the cache:\n%s", cacheCleanWord, answer.String())
}
}

func TestCacheQuestionNamesRetainedLegacyLocation(t *testing.T) {
t.Setenv(home.EnvVar, t.TempDir())
path := home.Join("workspace-caches", "legacy", "cache")
if err := os.MkdirAll(path, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "compiler"), []byte("cache"), 0600); err != nil {
t.Fatal(err)
}
var shown strings.Builder
if err := runCacheWith(nil, strings.NewReader(""), &shown); err != nil {
t.Fatal(err)
}
if !strings.Contains(shown.String(), home.Join("workspace-caches")) {
t.Fatalf("cache display omitted affected old location: %s", shown.String())
}
commentary := captureAside(t)
var output strings.Builder
if err := runCacheWith([]string{"clean"}, strings.NewReader("no\n"), &output); err != nil {
t.Fatal(err)
}
if !strings.Contains(commentary.String(), home.Join("workspace-caches")) {
t.Fatalf("cleanup confirmation omitted affected old location: %s", commentary.String())
}
if _, err := os.Stat(filepath.Join(path, "compiler")); err != nil {
t.Fatal("confirmation cleaned cache without authorization")
}
}
49 changes: 49 additions & 0 deletions cmd/codeaf/chatv3_initial_idle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/Agent-Field/codeaf/internal/cell"
"github.com/Agent-Field/codeaf/internal/cell/celltest"
"github.com/Agent-Field/codeaf/internal/session"
)

func TestNewChatSealsBackgroundRecordWithoutFirstForegroundTurn(t *testing.T) {
celltest.Available(t)
t.Setenv("CODEAF_HOME", t.TempDir())
c, err := cell.CreateIn(t.TempDir(), cell.Options{Class: cell.HostBound})
if err != nil {
t.Fatal(err)
}
seat, _, watch := v3SeatOf(session.Place{Dir: c.Root, Workspace: t.TempDir()}, nil)
closeClassSeat(t, seat)
if watch == nil {
t.Fatal("new chat has no seal watch")
}
// Observe the tail-seal callback at the real seat-construction seam. This
// proves scheduling only; the public terminal relay test proves transport.
checkpoint := filepath.Join(c.Root, "background-record.json")
sealed := make(chan string, 1)
watch.Quiet = 10 * time.Millisecond
watch.OnTail = func() {
raw, err := os.ReadFile(checkpoint)
if err == nil {
sealed <- string(raw)
}
}
if err := os.WriteFile(checkpoint, []byte(`{"state":"finished"}`), 0600); err != nil {
t.Fatal(err)
}
watch.Wrote()
select {
case got := <-sealed:
if got != `{"state":"finished"}` {
t.Fatalf("sealed stale background record: %s", got)
}
case <-time.After(time.Second):
t.Fatal("background record before the first foreground turn never reached the tail sealer")
}
}
30 changes: 20 additions & 10 deletions cmd/codeaf/chatv3_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,16 +487,22 @@ func v3MintSessionAs(class cell.Class, bucket, workspace, launchDir string, owne
return session.Place{}, err
}
place := v3PlaceFor(dir, workspace, owned)
// The write is not checked for the reason place.go gives: meta.json is a
// citation and not the record, and a session that would not open because a
// lookup file could not be written would be the wrong trade twice over.
_ = session.SaveMeta(dir, session.Meta{
ID: id,
Workspace: place.Workspace,
LaunchDir: launchDir,
Owned: owned,
Created: time.Now(),
})
var policy *session.WorkspacePolicy
if session.WorkspaceOwnershipPrototypeEnabled() {
policy = session.DefaultWorkspacePolicy()
}
// Placement policy is durable conversation truth, so a failed creation
// write must not silently open the conversation with legacy task placement.
if err := session.SaveMeta(dir, session.Meta{
ID: id,
Workspace: place.Workspace,
LaunchDir: launchDir,
Owned: owned,
Created: time.Now(),
WorkspacePolicy: policy,
}); err != nil {
return session.Place{}, fmt.Errorf("save new conversation policy: %w", err)
}
return place, nil
}

Expand Down Expand Up @@ -830,6 +836,10 @@ func v3SeatWith(place session.Place, say func(string), drives *driveBook) (execu
seat, err := cellstore.SeatOver(class, c, place.Workspace, observerOf(machine), watch.Report, driveStore(drive), cellstore.WithNotices(watch.Note), cellstore.WithPipeline(), cellstore.WithWarmCapture())
failed(watch, err)
watch.OnTail = func() { sealTail(seat, drive) }
// A fresh seat has no foreground turn in flight. Background tasks may be
// the first input; their journal writes need a tail seal even before chat.
// Window attachment reuses the engine's seat rather than constructing one.
watch.Waiting()
return executor.Gated(seat, driveGate(drive)), machine, watch
}

Expand Down
41 changes: 41 additions & 0 deletions cmd/codeaf/chatv3_layout_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -140,6 +142,7 @@ func TestTheGroomLeavesEveryConversationAlone(t *testing.T) {
// own id with meta.json already in it — so a second window sees the session
// before anybody has spoken in it.
func TestAFirstLaunchMintsAFolderThatNamesItself(t *testing.T) {
t.Setenv("CODEAF_PROTOTYPE_WORKSPACE_OWNERSHIP", "")
t.Setenv("CODEAF_HOME", filepath.Join(t.TempDir(), "state"))
workspace := t.TempDir()

Expand All @@ -154,6 +157,20 @@ func TestAFirstLaunchMintsAFolderThatNamesItself(t *testing.T) {
if err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(found.Place.MetaPath())
if err != nil {
t.Fatal(err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatal(err)
}
if _, exists := fields["workspace_policy"]; exists {
t.Fatalf("ordinary conversation persisted a workspace policy: %s", raw)
}
if meta.WorkspacePolicy != nil {
t.Fatal("ordinary conversation enabled private task copies")
}
if meta.ID != filepath.Base(found.Place.Dir) {
t.Fatalf("the folder is called %s and the session %s", filepath.Base(found.Place.Dir), meta.ID)
}
Expand Down Expand Up @@ -477,3 +494,27 @@ func TestUnreadableTeamsNeverAuthorizeEmptyCleanupOrReuse(t *testing.T) {
t.Fatal("unknown membership was removed")
}
}

func TestNewConversationPersistsPrivateTaskPolicyOnlyWithExperiment(t *testing.T) {
for _, flag := range []string{"", "1"} {
t.Run("switch="+flag, func(t *testing.T) {
t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv("CODEAF_PROTOTYPE_WORKSPACE_OWNERSHIP", flag)
place, err := v3MintSession(t.TempDir(), t.TempDir(), "", false)
if err != nil {
t.Fatal(err)
}
meta, err := session.LoadMeta(place.Dir)
if err != nil {
t.Fatal(err)
}
want := flag == "1" && (runtime.GOOS == "linux" || runtime.GOOS == "darwin")
if (meta.WorkspacePolicy != nil) != want {
t.Fatalf("policy=%+v, want enabled=%v", meta.WorkspacePolicy, want)
}
if want && (meta.WorkspacePolicy.Version != 1 || meta.WorkspacePolicy.Mode != "private-tasks") {
t.Fatalf("wrong policy: %+v", meta.WorkspacePolicy)
}
})
}
}
9 changes: 9 additions & 0 deletions cmd/codeaf/synctake.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ func (t takeover) TakeSeen(ctx context.Context, id string, seen *directory.Lease
return surfaceTaken(got, elapsed), nil
}

func (t takeover) TakeSeenWithWorkspaceRoots(ctx context.Context, id string, seen *directory.LeaseExpectation, roots map[string]string) (tui3.Taken, error) {
began := time.Now()
got, err := t.c.TakeSeenWithWorkspaceRoots(ctx, id, seen, roots)
if err != nil {
return tui3.Taken{}, err
}
return surfaceTaken(got, time.Since(began)), nil
}

func (t takeover) OpenBranch(ctx context.Context, id string) (tui3.Taken, error) {
began := time.Now()
got, err := t.c.OpenBranch(ctx, id)
Expand Down
4 changes: 4 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ specific action rather than reading either large package from beginning to end.
| Remote sessions and movable chats | [remote](../internal/remote/), [enginehost](../internal/enginehost/), [cell](../internal/cell/) | [Remote architecture](REMOTE.md), [remote testing](testing-anywhere.md) |
| Relay coordination across computers and workers | [relay server](../internal/relayserve/), [committed directory](../internal/raftdirectory/), [consensus core](../internal/raftcore/), [hosted relay](../relay/hosted/), [cluster command](../cmd/codeaf/cluster.go) | [Run a relay](relay.md), [coordination and workers](../internal/manual/chat/coordination-and-workers.md) |

Before delegating across folders, cooperating on private copies, receiving a chat,
applying results or cleaning task storage, read the
[task workspace and relay agent contract](architecture/workspaces-storage-relay.md).

## Find the right storage and knowledge owner

There is no single Markdown file that acts as every kind of memory. Keep these
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ notes and users' learned chat knowledge are separate; see [documentation ownersh

Start with the [current architecture map](ARCHITECTURE.md). It identifies the
chat and resident surfaces, entry points, implementation owners and storage roles.
The [workspace storage and relay reference](architecture/workspaces-storage-relay.md)
explains task folders, cooperating layouts, multi-root transfer, and result application.

| Category | References |
| --- | --- |
Expand Down
Loading
Loading