Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3ba1d8f
Honor chat-requested models for task workers and new conversations (#…
santoshkumarradha Oct 25, 2026
c4bd257
Answer and verify the four skill capability questions (#1938)
santoshkumarradha Oct 25, 2026
0e10604
Make All show the current conversation tabs (#1939)
santoshkumarradha Oct 25, 2026
7f5da93
Run the same program when connecting to another machine (#1940)
santoshkumarradha Oct 25, 2026
9ada680
Add linked project knowledge vault to Memory (#1943)
santoshkumarradha Oct 25, 2026
5fb1ddf
docs: organize product, developer, and contributor knowledge (#1945)
santoshkumarradha Oct 25, 2026
f6dc162
Fix chat startup, blank tab switches, and delayed team workers (#1946)
santoshkumarradha Oct 25, 2026
a08b936
fix(tui): keep tabs usable while opening a chat (#1947)
santoshkumarradha Oct 26, 2026
1d028bf
Keep startup and resume responsive during storage and engine delays (…
santoshkumarradha Oct 26, 2026
ed8bf75
fix(tui): keep team navigation responsive during first worker open (#…
santoshkumarradha Oct 26, 2026
e88e49a
Make Add another device clickable with an editable pairing field (#1949)
santoshkumarradha Oct 26, 2026
808c90c
Relay: coordination as a Raft group, and joined computers can lead (#…
AbirAbbas Oct 26, 2026
f0cf232
Bump embedded furrow to v0.3.3 (fast store open) (#1962)
santoshkumarradha Oct 26, 2026
b30e0b9
perf(engine): keep furrow detection off the session build path (#1954)
santoshkumarradha Oct 26, 2026
334cab9
feat(knowledge): replace Python with native background builds (#1966)
santoshkumarradha Oct 26, 2026
4b03be9
Make the home add-device card clearer and clickable (#1968)
santoshkumarradha Oct 26, 2026
08b922b
fix(session): a swept fork stays retired (#1969)
santoshkumarradha Oct 26, 2026
9e34209
Two computers: continue a moved chat, clean leader quit, removed-devi…
AbirAbbas Oct 26, 2026
f961fbc
perf(enginehost): a retiring host gives the workspace up before it sa…
santoshkumarradha Oct 26, 2026
6ef871a
Bump embedded furrow to v0.3.4 (prune abandoned capture plans) (#1974)
santoshkumarradha Oct 26, 2026
cf282dd
fix(cellstore): never capture a folder that holds the home folder (#1…
santoshkumarradha Oct 26, 2026
4326eb3
perf: an idle client stops reopening the vault per key and retrying f…
santoshkumarradha Oct 26, 2026
d0ff650
Build reusable knowledge guides from saved history (#1975)
santoshkumarradha Oct 26, 2026
ff5543f
Private task copies (opt-in), frame hints through the Raft relay, and…
santoshkumarradha Oct 26, 2026
644a38e
Two-computer relay: fast take-back, moved task pages, honest saving, …
AbirAbbas Oct 27, 2026
0447a05
Maintain coherent knowledge guides and reuse validated resolver work …
santoshkumarradha Oct 27, 2026
62ea76d
Reuse unaffected knowledge evidence groups after corrections (#2000)
santoshkumarradha Oct 27, 2026
cc1aaa8
Quitting right after work saves quietly and never branches bookkeepin…
AbirAbbas Oct 27, 2026
3bf6072
fix(sync): never copy an older chat that works in the home folder (#2…
santoshkumarradha Oct 27, 2026
af03038
fix: honor explicit fallbacks and preserve finished shell calls (#2020)
AbirAbbas Oct 28, 2026
66fa77e
fix: reject malformed write payloads before changing files (#2028)
santoshkumarradha Oct 29, 2026
b8f62f6
cellstore: background seal notices go to the log, not through the cha…
santoshkumarradha Oct 29, 2026
9508f1e
fix: --host over ssh no longer evicts a session host started with COD…
AbirAbbas Oct 30, 2026
ce06d29
feat(settings): a Devices tab — add, rename and remove computers from…
AbirAbbas Oct 30, 2026
34ced44
Reach a paired computer through the relay sync uses (#2073)
santoshkumarradha Oct 30, 2026
9a03226
Never lock a person out of their own chat on an older sync service (#…
santoshkumarradha Oct 30, 2026
6206dbc
fix(relay): harden the machine doors #2073 put on the hosted relay (#…
AbirAbbas Oct 30, 2026
f128ec3
fix: make dev's Full check green (promotion tests, archived relay cli…
AbirAbbas Oct 30, 2026
f3e4f6f
Continue a chat from another computer every time, and let doctor repa…
AbirAbbas Oct 30, 2026
72a2792
One relay per release channel, deployed by CI when relay code changes…
AbirAbbas Oct 30, 2026
70c5c62
Sync off keeps every chat, and a copied older chat can no longer be w…
AbirAbbas Oct 31, 2026
152134c
codeaf devbox: an always-on computer on your own Railway account (#2081)
AbirAbbas Oct 31, 2026
9638112
Fix what the Mac tester found: a deleted home is never rewritten, mac…
AbirAbbas Oct 31, 2026
9be6706
A displaced window keeps its unsent turns, and a taken chat can be ta…
AbirAbbas Oct 31, 2026
b42fdf1
teams: opt-in team sharing between paired computers (#2080)
AbirAbbas Oct 31, 2026
4e42800
Continue the chat a person picks; keep the retry card; honest waits o…
AbirAbbas Oct 31, 2026
23f7edd
Run a task on another paired computer while the chat stays here (#2085)
AbirAbbas Oct 31, 2026
778ee15
test: remote-task session fixtures pass on macOS temporary folders (#…
AbirAbbas Oct 31, 2026
20eb695
Pairing, devices and sync connection: say the true thing, and ask bef…
AbirAbbas Oct 31, 2026
f747f36
Merge origin/dev into app/v3 (#2020 through #2092)
santoshkumarradha Nov 1, 2026
ce8d5b1
Merge follow-up: land dev's fixes on app/v3's stores and pairing
santoshkumarradha Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
76 changes: 71 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ jobs:
fi
test_args=()
[ -z "$skip" ] || test_args=(-skip "^(${skip})$")
go test "${test_args[@]}" \
# Bash 3.2 treats an empty array as unbound under nounset, so expand it conditionally.
go test ${test_args[@]+"${test_args[@]}"} \
./cmd/codeaf \
./internal/config \
./internal/exec/... \
Expand Down Expand Up @@ -255,16 +256,81 @@ jobs:
path: dist/*
if-no-files-found: error

# EACH CHANNEL DEPLOYS ITS OWN RELAY BEFORE ITS BINARIES PUBLISH. dev updates
# caf-fabric-dev, staging updates caf-fabric-staging, and main (rc) or a
# stable dispatch updates caf-fabric once RELAY_DEPLOY_PRODUCTION is true. A
# failed deploy or post-deploy proof rolls the Worker back when a prior
# version existed and stops publish; a rollback cannot undo a one-way storage
# upgrade, which is why every relay change reaches dev first and staging a
# week later.
relay:
needs: prepare
permissions:
contents: read
concurrency:
group: relay-${{ (needs.prepare.outputs.channel == 'stable' || needs.prepare.outputs.channel == 'rc') && 'production' || needs.prepare.outputs.channel }}
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check production relay deploy gate
id: gate
env:
CHANNEL: ${{ needs.prepare.outputs.channel }}
RELAY_DEPLOY_PRODUCTION: ${{ vars.RELAY_DEPLOY_PRODUCTION }}
run: |
set -euo pipefail
if [ "$CHANNEL" = "stable" ] || [ "$CHANNEL" = "rc" ]; then
if [ "$RELAY_DEPLOY_PRODUCTION" != "true" ]; then
echo "Production relay deploys are off; set repository variable RELAY_DEPLOY_PRODUCTION=true to enable caf-fabric deploys."
echo "deploy=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
echo "deploy=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.gate.outputs.deploy == 'true'
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.sha }}
- uses: actions/setup-go@v5
if: steps.gate.outputs.deploy == 'true'
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@v4
if: steps.gate.outputs.deploy == 'true'
with:
node-version: "22"
cache: npm
cache-dependency-path: relay/hosted/package-lock.json
- name: Install hosted relay dependencies
if: steps.gate.outputs.deploy == 'true'
working-directory: relay/hosted
run: npm ci
- name: Deploy and verify channel relay
if: steps.gate.outputs.deploy == 'true'
env:
CHANNEL: ${{ needs.prepare.outputs.channel }}
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
RELAY_DEPLOY_PRODUCTION: ${{ vars.RELAY_DEPLOY_PRODUCTION }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CODEAF_TELEMETRY: "off"
DO_NOT_TRACK: "1"
run: bash scripts/relay-deploy.sh

# PUBLISH SAYS always() FOR THE SAME REASON build DOES. GitHub skips a job
# whose dependency chain contains a skipped job unless the job's own condition
# says otherwise, and the chain here always contains one: `test` is skipped on
# every dev build by design. The first live run on 2026-09-11 (dev, ef36c3770)
# proved it — prepare and build green, eight minutes of binaries uploaded, and
# publish skipped without a word, so no dev release existed. The two explicit
# results below are the whole gate: a red build still publishes nothing.
# publish skipped without a word, so no dev release existed. The three
# explicit results below are the whole gate: a red build or relay deploy still
# publishes nothing.
publish:
needs: [prepare, build]
if: always() && needs.prepare.result == 'success' && needs.build.result == 'success'
needs: [prepare, build, relay]
if: always() && needs.prepare.result == 'success' && needs.build.result == 'success' && needs.relay.result == 'success'
permissions:
contents: write
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ test-tooling:
bash scripts/touched-verdict_test.sh
bash scripts/one-suite_test.sh
bash scripts/shard-test_test.sh
bash scripts/relay-fingerprint_test.sh
bash scripts/relay-wrangler-config_test.sh
bash scripts/relay-deploy_test.sh
bash bench/canary/lib/repo_test.sh

manual-gates:
Expand Down
25 changes: 25 additions & 0 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,31 @@ work. Preserve those assertions when changing a hot path. Paint must not perform
filesystem or network reads. Document any cap change with its rationale in the
same commit; derive repeated values from constants.

## Paired team history

These sharing budgets apply only when **share teams** is on in Settings → Devices.
Sharing is off by default on each computer; off rounds run no team carriers,
identity discovery, mirror writes or ownership lookups and retain existing polling.

`teams.TrafficSyncLines` limits each writing computer's window to 32 lines per
team, and `teams.TrafficSyncBytes` limits encoded lines to 16 KiB per team.
`teams.TrafficSyncTextBytes` shortens text beyond 2 KiB at a rune boundary.
`teams.TrafficSyncDeviceBytes` caps each device's encoded Traffic share at 64 KiB
across teams, newest local activity first. `teams.TrafficSyncEvery` permits a
publication at most once every 30 seconds; team record edits are not delayed.
Capture reads the bounded backward tail across the current and rotated logs. Received windows replace a
separate display mirror and are never republished. An unchanged round on 500
chats and 5 teams performs zero world scans and opens zero cell metadata files;
member identities cache by metadata stamp, unchanged authored tails are reused,
and unresolved incoming identities share at most one discovery pass per round. Tests count scans and encoded bytes,
compare deterministic values, and verify quiet rounds leave the vault unchanged.
Mirror filtering makes zero directory requests per vault round, using only an
already-held local roster, or every other writer when none is available. A send
shares one ownership reading across its recipients, with a 3-second timeout and
a 5-second process cache (including failures). Cursor saves stat identity files
but decode them only after a stamp changes. Mirror refresh never acknowledges
local news and each hosted window carries its own mirror version.

## Binary size

`SIZE-BUDGET` is the stripped binary's byte cap. `make size` builds through
Expand Down
7 changes: 7 additions & 0 deletions cmd/codeaf-release/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// codeaf-release next --channel dev|staging --sha <commit> --date YYYYMMDD
// codeaf-release kind <tag>
// codeaf-release prune --channel dev|staging
// codeaf-release service [--url <sync service>]
package main

import (
Expand Down Expand Up @@ -46,6 +47,10 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 0
case "prune":
err = runPrune(args[1:], stdin, stdout)
case "service":
err = runService(args[1:], stdout)
case "relay-url":
err = runRelayURL(args[1:], stdout)
case "promotion-plan":
err = runPromotionPlan(args[1:], stdout)
case "promotion-message":
Expand Down Expand Up @@ -90,6 +95,8 @@ func usage(w io.Writer) {
Print stable, rc, dev, staging, or other.
prune --channel dev|staging
Read <tag><tab><RFC3339 createdAt> lines and print expired tags.
service [--url <sync service>] | relay-url --channel stable|rc|staging|dev
Check a live sync service's capabilities, or print a channel's hosted URL.
promotion-plan|promotion-decision|promotion-recheck|promotion-release|promotion-published|promotion-jobs|promotion-push-error|promotion-message
Plan, check, and report the staging promotion. See docs/rules/promotion.md.
`)
Expand Down
69 changes: 69 additions & 0 deletions cmd/codeaf-release/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"context"
"fmt"
"io"
"strings"
"time"

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

// serviceProbeWithin bounds the live check, so a silent service fails the
// release instead of holding the workflow.
const serviceProbeWithin = 60 * time.Second

// probeService is the live check; tests replace it.
var probeService = syncsetup.ProbeService

// runService fails when the sync service a build will talk to does not promise
// what the build needs. A client never ships ahead of its service (#2072).
func runService(args []string, stdout io.Writer) error {
url := syncsetup.HostedRelayURL
switch {
case len(args) == 2 && args[0] == "--url":
url = args[1]
case len(args) != 0:
return usageErr("service takes only --url <sync service>")
}
ctx, cancel := context.WithTimeout(context.Background(), serviceProbeWithin)
defer cancel()
missing, err := probeService(ctx, url)
if err != nil {
return fmt.Errorf("could not check the sync service at %s: %w", url, err)
}
if len(missing) > 0 {
return fmt.Errorf("the sync service at %s must be updated before this build ships; it does not promise %s", url, describe(missing))
}
fmt.Fprintf(stdout, "the sync service at %s promises everything this build needs\n", url)
return nil
}

func describe(missing []directory.Capability) string {
parts := make([]string, len(missing))
for i, c := range missing {
parts[i] = fmt.Sprintf("%s (needed for %s)", c.Header, c.Feature)
}
return strings.Join(parts, ", ")
}

// runRelayURL prints the hosted sync service address for one release channel so
// CI scripts read internal/syncsetup/relayurl.go instead of spelling URLs twice.
func runRelayURL(args []string, stdout io.Writer) error {
if len(args) != 2 || args[0] != "--channel" {
return usageErr("relay-url takes --channel <stable|rc|staging|dev>")
}
switch args[1] {
case "stable", "rc":
fmt.Fprintln(stdout, syncsetup.ProductionRelayURL)
case "staging":
fmt.Fprintln(stdout, syncsetup.StagingRelayURL)
case "dev":
fmt.Fprintln(stdout, syncsetup.DevRelayURL)
default:
return usageErr("unknown channel %q (expected stable, rc, staging, or dev)", args[1])
}
return nil
}
53 changes: 53 additions & 0 deletions cmd/codeaf-release/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"bytes"
"context"
"strings"
"testing"

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

// TestServiceGateFailsAheadOfTheService pins #2072's release gate: a build
// whose service lacks a promise does not ship, and the refusal names it.
func TestServiceGateFailsAheadOfTheService(t *testing.T) {
defer func(was func(context.Context, string) ([]directory.Capability, error)) { probeService = was }(probeService)
probeService = func(context.Context, string) ([]directory.Capability, error) {
return directory.Capabilities, nil
}
var out, errs bytes.Buffer
if code := run([]string{"service", "--url", "https://sync.example"}, nil, &out, &errs); code != 1 {
t.Fatalf("exit %d, stderr %q", code, errs.String())
}
if !strings.Contains(errs.String(), directory.ConditionalTakeHeader) {
t.Fatalf("the refusal does not name the promise: %q", errs.String())
}
probeService = func(context.Context, string) ([]directory.Capability, error) { return nil, nil }
if code := run([]string{"service"}, nil, &out, &errs); code != 0 {
t.Fatalf("a complete service failed the gate: %q", errs.String())
}
}

func TestRelayURLPrintsEachChannelConstant(t *testing.T) {
cases := map[string]string{
"stable": syncsetup.ProductionRelayURL,
"rc": syncsetup.ProductionRelayURL,
"staging": syncsetup.StagingRelayURL,
"dev": syncsetup.DevRelayURL,
}
for ch, want := range cases {
var out, errs bytes.Buffer
if code := run([]string{"relay-url", "--channel", ch}, nil, &out, &errs); code != 0 {
t.Fatalf("relay-url --channel %s exited %d: %s", ch, code, errs.String())
}
if got := strings.TrimSpace(out.String()); got != want {
t.Fatalf("relay-url --channel %s = %q, want %q", ch, got, want)
}
}
var out, errs bytes.Buffer
if code := run([]string{"relay-url", "--channel", "unknown"}, nil, &out, &errs); code != 2 {
t.Fatalf("unknown channel exited %d, want 2", code)
}
}
7 changes: 5 additions & 2 deletions cmd/codeaf/app_chat_rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,12 @@ func TestAppChatRowsR1P2PresenceIsIndependentOfClosedChat(t *testing.T) {
for _, row := range machineCall[appwire.ChatsResult](t, wire, "chats.list", struct{}{}).Chats {
rows[row.Cell] = row
}
for _, id := range ids[:2] {
// A lapsed lease of this computer's own is free rather than off (#2072),
// so the chat this backend made reads idle; presence is reported either way.
for i, id := range ids[:2] {
row := rows[id]
if row.Status != "off" || row.Online == nil || !*row.Online || row.Line != "" || row.TakeoverLine != "" {
want := map[int]string{0: "off", 1: "idle"}[i]
if row.Status != want || row.Online == nil || !*row.Online || row.Line != "" || row.TakeoverLine != "" {
t.Errorf("R1 online holder with closed chat: %+v", row)
}
}
Expand Down
11 changes: 4 additions & 7 deletions cmd/codeaf/appkeylate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ func TestAppAProcessThatOpenedWithoutAKeyOpensOnceOneArrives(t *testing.T) {
}
}

// EVERY OTHER DOOR KEEPS ITS FIRST ANSWER. A pipe or a host reads its key
// before it serves anything, so the memo hands every later conversation the
// open it already made, failed or not, and only the app's door opens again.
func TestOnlyTheAppsDoorOpensAFailedProcessHalfAgain(t *testing.T) {
// A FAILED OPEN IS TRIED AGAIN ON EVERY DOOR. A host's first connection can
// precede its first vault pull and the app is handed its key after it starts,
// so a failure is never the memo's answer; a success is, and is shared.
func TestEveryDoorOpensAFailedProcessHalfAgain(t *testing.T) {
root := resolvedTempDir(t)
wakeEnvironment(t, root)
t.Setenv("OPENROUTER_API_KEY", "")
Expand All @@ -69,9 +69,6 @@ func TestOnlyTheAppsDoorOpensAFailedProcessHalfAgain(t *testing.T) {
t.Fatal("the process half opened with no key anywhere")
}
t.Setenv("OPENROUTER_API_KEY", "test-key")
if _, err := openEngineProcess(); err == nil {
t.Fatal("a door that is not the app opened its process half a second time")
}
// Callers racing to open it again build ONE process half between them.
opened := make([]*v3Process, 8)
var wg sync.WaitGroup
Expand Down
Loading