Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2c7f108
processgroup: macOS reads process groups, so left-running services an…
AbirAbbas Nov 3, 2026
d19f0ff
factory: mark a draft pull request ready from the app (#2238)
santoshkumarradha Nov 3, 2026
8567959
factory: cheap and strong steps run on the small work and checks mode…
santoshkumarradha Nov 3, 2026
a3357f3
factory: Backlog jobs say which protected paths they name (factory.jo…
santoshkumarradha Nov 3, 2026
4bc0850
factory: a no-change comment finishes Follow PR, and a Try again that…
santoshkumarradha Nov 3, 2026
5850671
config: Model Pool stays off until someone turns it on (#2257)
santoshkumarradha Nov 3, 2026
76c9681
perf(knowledge): capture wakes on the chat's own writes, not a per-ch…
AbirAbbas Nov 3, 2026
e1a3692
tests: sync fixtures keep their socket homes short on macOS and wait …
AbirAbbas Nov 3, 2026
a4d4a56
remote: an app chat's streamed text is folded before it is framed (#2…
AbirAbbas Nov 3, 2026
bb9fad4
factory: a write step that changed nothing is not done (#2158)
santoshkumarradha Nov 3, 2026
54bef3d
factory: a job's budget ask offers one amount (#2258)
santoshkumarradha Nov 3, 2026
158a8ed
session: a note sent during a final answer gets its read receipt (#2259)
santoshkumarradha Nov 3, 2026
b39a560
factory: a job's Brief counts a person's later requests as asked (#2262)
santoshkumarradha Nov 3, 2026
17ccbdd
test: two process and pipeline intermittents wait on the right signal…
santoshkumarradha Nov 3, 2026
3d0ce97
factory: job memory calls count in its header and budget (#2261)
santoshkumarradha Nov 3, 2026
ceb5082
memory: off by default, one switch per home (#2266)
santoshkumarradha Nov 3, 2026
d0d2ce3
keys: a failed vault read returns its error instead of panicking (#2265)
santoshkumarradha Nov 3, 2026
ade1233
app: one engine on a home; a second waits for the first to end (#2274)
santoshkumarradha Nov 3, 2026
6405b06
spend: a failed or cut-off call still counts (#2267)
santoshkumarradha Nov 3, 2026
909c9db
spend: today's figure moves with every ledger row and names Factory A…
santoshkumarradha Nov 3, 2026
af0fe9a
spend: Factory helper calls count on the job and the page (#2268)
santoshkumarradha Nov 3, 2026
a722531
spend: a hedge's finished loser counts, with proofs for brief, Follow…
santoshkumarradha Nov 3, 2026
252ecb8
factory: a plan approval stays on the job through later gates (#2276)
santoshkumarradha Nov 3, 2026
b8c64d8
session: remove stray lines a stacked landing left in usage_ledger.go…
santoshkumarradha Nov 3, 2026
ca73f86
Memory list says where to add a model key (#2278)
santoshkumarradha Nov 3, 2026
e13ee2a
Nothing automatic spends on an issue closed on GitHub (#2282)
santoshkumarradha Nov 3, 2026
19f4df4
Repo AI notes and themes count as Factory AI (#2281)
santoshkumarradha Nov 3, 2026
63cff5f
A job has one manager, and all its spend counts against its budget (#…
santoshkumarradha Nov 3, 2026
1870bce
Merge app/v3 into workstreams/main
AbirAbbas Nov 3, 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ ChatGPT plan, Ollama and any OpenAI-compatible endpoint.

## Model Pool

Installs can pool what they have measured about models. It is on by
default: what an install sends is computed, text-free numbers about the models
Installs can pool what they have measured about models. It is off by
default. Turn it on with `model_pool = on` in settings or
`CODEAF_MODEL_POOL=on`. When enabled, what an install sends is computed, text-free numbers about the models
it ran (role, model, a number, which model judged, door, size bucket, day) under
a per-install nonce, never code,
prompts, paths or an identity, and `codeaf pool status` shows exactly what is
Expand Down
5 changes: 5 additions & 0 deletions cmd/codeaf-replay/replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ func TestEveryTagTheBuildWritesResolvesToARoleSomebodyDeclared(t *testing.T) {
// that passes forever the moment somebody builds a tag out of a variable, which
// is exactly how the log came to carry 2,309 untagged rows in the first place.
var derivedTags = map[string]func(yield func(tag, where string)){
// internal/knowledge/compiler_provider.go tags each compile call with the
// lane role memory runs under; that is the whole of what it can be.
"string(lanes.RoleMemory)": func(yield func(tag, where string)) {
yield("memory", "internal/knowledge/compiler_provider.go, a memory compile call")
},
// internal/session/toolask.go: a tool's own call carries the tool's name
// after the role word, and no walk can enumerate the belt.
"toolCallTag(question.tool)": func(yield func(tag, where string)) {
Expand Down
40 changes: 37 additions & 3 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ func runApp(args []string) error {
return err
}
}
if err := holdAppHome(); err != nil {
return err
}
// STDOUT IS THE PROTOCOL'S AND NOBODY ELSE'S. The wire keeps the real
// descriptor, and anything else in this process that prints to stdout
// lands on stderr, where the app collects logs.
Expand Down Expand Up @@ -175,12 +178,16 @@ func runApp(args []string) error {
if *turnLong > 0 {
backend.turnLong = *turnLong
}
defer backend.watchSpendRows()()
// The crews are watched from the start: a manager's start or a member
// woken by its manager must not wait for the window to ask for crews.
backend.memoryStore = func() (*store.Store, error) {
proc, err := openAppEngineProcess()
if err != nil || proc.Memory == nil {
return nil, errors.New("memory is off")
if err != nil {
return nil, err
}
if proc.Memory == nil {
return nil, errors.New("this engine has no saved memory store")
}
return proc.Memory, nil
}
Expand Down Expand Up @@ -368,6 +375,9 @@ func appOpenInProcess(ask appOpenAsk) (*appConn, error) {
}
eng, err := assembleEngine(h, workspace, transcript, workspace, ask.IntentSiblingNames)
if err == nil {
// In-process chats are the desktop's hot path. Remote surfaces keep
// one frame per event until someone measures them.
eng.DeltaWindow = remote.DefaultDeltaWindow
err = enableAppConversationModels(eng.Agent, ask.Models)
}
if err == nil && ask.AutomaticModels != nil {
Expand Down Expand Up @@ -707,6 +717,9 @@ type appBackend struct {
dayAuth appDayAuthority
// today replaces the usage ledger's figure for today in a test.
today func(time.Time) float64
// spendRowsPending holds one read of today's figure for rows written
// together (watchSpendRows).
spendRowsPending atomic.Bool
// turnLong is how long a reply runs before turn.long ([appTurnLong]).
turnLong time.Duration
// inbox is every open ask as one list (appinbox.go); emit is its tap.
Expand Down Expand Up @@ -4242,7 +4255,15 @@ func (b *appBackend) SpendToday() (appwire.Spend, error) {
if err != nil {
return appwire.Spend{}, appEngineError(err)
}
return appwire.Spend{TodayUSD: spent, DayCapUSD: appFigure(limit)}, nil
spend := appwire.Spend{TodayUSD: spent, DayCapUSD: appFigure(limit)}
if b.today == nil {
reading, err := session.ReadDailySpend(session.UsageLedgerPath(), now)
if err != nil {
return appwire.Spend{}, appEngineError(err)
}
spend.FactoryAIUSD = appFigure(reading.FactoryAIUSD)
}
return spend, nil
}

// appSpendSettle is how long after a turn ends its spending is read again: the
Expand Down Expand Up @@ -4275,6 +4296,19 @@ func (b *appBackend) spendMoved(after func(appwire.Spend)) {
})
}

// watchSpendRows says spend.changed for ledger rows no turn ended with. Rows
// that arrive together cost one read. The answer stops the watching.
func (b *appBackend) watchSpendRows() (stop func()) {
return session.OnUsageWritten(func() {
if b.spendRowsPending.CompareAndSwap(false, true) {
time.AfterFunc(appSpendSettle, func() {
b.spendRowsPending.Store(false)
b.spendMoved(nil)
})
}
})
}

// ListTasks reads the agent's tasks off the disk whether or not its
// conversation is held — its own checkpoint first, which holds every task it
// ever had, finished ones included, then its project's task index for any the
Expand Down
8 changes: 7 additions & 1 deletion cmd/codeaf/app_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,13 @@ func (h *appFactoryHost) runWorker(ctx context.Context, target factoryTarget) {
turns := factoryShapeTurnsFrom(h.floor, h.workspace, h.profile, current)
turns.refusal = source.Refusal
turns.liveOK = factoryLiveOK(raw, h.profile)
runner := buildFactoryRunner(mirror, h.floor, h.workspace, h.profile, h.stepMaker(mirror, source), turns)
maker := h.stepMaker(mirror, source)
if target.url == h.address {
// Memory admission reads Pause and Stop before their worker signal arrives.
// Receipts still book on the mirror and follow its ordinary settlement path.
maker.memoryOwner = h.floor
}
runner := buildFactoryRunner(mirror, h.floor, h.workspace, h.profile, maker, turns)
dial := h.runnerDial(target)
machine, _ := os.Hostname()
kind := api.RunnerCI
Expand Down
51 changes: 51 additions & 0 deletions cmd/codeaf/app_home.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"fmt"
"os"
"time"

"github.com/Agent-Field/codeaf/internal/filelock"
"github.com/Agent-Field/codeaf/internal/home"
)

// appHomeLock stays reachable until process death. Closing it when runApp
// returns would admit a new engine while main still finishes saving. The OS
// releases it on any exit, so a crash leaves nothing to clean up.
var appHomeLock *os.File

// appHomeWait is how long an engine waits for one that is closing the same home.
// It outlasts the longest careful wrap-up, so an update's replacement engine
// starts as soon as the old one ends.
var appHomeWait = appLeaveWait + 10*time.Second

const appHomePoll = 50 * time.Millisecond

// holdAppHome makes this the one engine on this home. Two engines would both
// spend on the same work. It runs before anything that could make a paid call.
func holdAppHome() error {
if err := os.MkdirAll(home.Dir(), 0700); err != nil {
return err
}
file, err := os.OpenFile(home.Join("app.lock"), os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
}
deadline := time.Now().Add(appHomeWait)
for {
err := filelock.Lock(file, true, true)
if err == nil {
appHomeLock = file
return nil
}
if !filelock.IsBusy(err) {
_ = file.Close()
return fmt.Errorf("hold codeaf app home: %w", err)
}
if time.Now().After(deadline) {
_ = file.Close()
return fmt.Errorf("another codeaf app is still closing this home after %s; nothing was started. Quit it, then open the app again", appHomeWait)
}
time.Sleep(appHomePoll)
}
}
3 changes: 3 additions & 0 deletions cmd/codeaf/app_lend.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ func appLendLive(agent *session.Agent, ask appOpenAsk, hello remote.Hello) (*app
SessionFile: ask.Transcript,
Resumed: true,
}
// In-process chats are the desktop's hot path. Remote surfaces keep
// one frame per event until someone measures them.
engine.DeltaWindow = remote.DefaultDeltaWindow
sess := remote.NewSession(engine, true)
loop, err := remote.LoopbackAttach(hello, remote.AttachOptions{
Open: func(remote.Hello) (*remote.Session, error) { return sess, nil },
Expand Down
Loading
Loading