Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0ac4fa6
ci: gate pull requests into and pushes to workstreams/main
AbirAbbas Nov 3, 2026
c767483
app: workstream wire contract v0 (types, handlers, not-yet stubs) (#2…
AbirAbbas Nov 3, 2026
f4078a9
app: workstream pages render from editor JSON to markdown (pagedoc) (…
AbirAbbas Nov 3, 2026
d7456f8
app: workstream store (schema 3, factory_job refs, all 25 methods ser…
AbirAbbas Nov 3, 2026
7232f12
tests: workstream methods keep the wire contract's shapes (conformanc…
AbirAbbas Nov 3, 2026
1870bce
Merge app/v3 into workstreams/main
AbirAbbas Nov 3, 2026
dd749e1
app: workstream pages read by version, restorable delete, Spec/Decisi…
AbirAbbas Nov 3, 2026
4dcd515
app: workstream agent prompt and tools (proposals, drafts, its own pa…
AbirAbbas Nov 3, 2026
c54b647
ci: cmd/codeaf finishes inside its touched leg (busy case 200 ms, cod…
AbirAbbas Nov 3, 2026
c1b540d
app: workstreams wire v0.2: draft size/budget/source, page sources, r…
AbirAbbas Nov 3, 2026
b79efbf
workstreams: the database refuses deleting a workstream's collection;…
AbirAbbas Nov 3, 2026
5cc5a28
workstreams: size limits on plans, pages and records; agent Spec/Deci…
AbirAbbas Nov 3, 2026
4a9bdd3
Merge app/v3 into workstreams/main
AbirAbbas Nov 3, 2026
f5bf32e
workstreams: a job's spend counts its earlier runs
AbirAbbas Nov 3, 2026
14c3f0a
workstreams: the earlier-runs spend test names its agent spend as a f…
AbirAbbas Nov 3, 2026
27a989e
workstreams: the watching agent (morning and job-event wakes, undoabl…
AbirAbbas Nov 3, 2026
d33dfff
workstreams: sources (file, repo, issue, link, kept), wire v0.4, sche…
AbirAbbas Nov 3, 2026
e75aa1f
app: workstream suggested edits (wire v0.5: suggestion.list/keep/drop…
AbirAbbas Nov 3, 2026
cf3074c
Merge app/v3 into workstreams/main
AbirAbbas Nov 3, 2026
e9a571c
Client tools: a client attaches tools to a conversation; collection-h…
AbirAbbas Nov 3, 2026
fe68c7d
app: the engine keeps only the generic pieces of the Workstreams plan…
AbirAbbas Nov 3, 2026
4b1bb03
A blank conversation a collection holds is kept, never resumed, and s…
AbirAbbas Nov 4, 2026
211c968
Merge app/v3 into workstreams/main
AbirAbbas Nov 4, 2026
b1a02be
Client tools: a tool call names the message its turn answers (#2346)
AbirAbbas Nov 4, 2026
4d6562c
Merge app/v3 into workstreams/main
AbirAbbas Nov 4, 2026
25c6faa
Client tools name their answer cutoff a wait, so the leaf-room law pa…
AbirAbbas Nov 4, 2026
18ce0fb
Merge app/v3 into workstreams/main
AbirAbbas Nov 5, 2026
731486d
Pass the keep argument in the stale-fork cancellation test
AbirAbbas Nov 5, 2026
1e429b2
turn.tool carries the conversation's live spend so far (#2413)
AbirAbbas Nov 5, 2026
0c5e76d
A resumed conversation restores its late receipts, so it never saves …
AbirAbbas Nov 5, 2026
1013f05
Factory job outcomes: a durable cursor read and a pushed factory.even…
AbirAbbas Nov 5, 2026
08b6e1e
Client tools cross the remote door to the connection that set them (#…
AbirAbbas Nov 5, 2026
28335c3
Factory: a job can wait on other jobs' merges (#2418)
AbirAbbas Nov 5, 2026
e86fd18
factory.job_create: issue choice, labels, starting budget, idempotenc…
AbirAbbas Nov 5, 2026
9367e93
factory.repo_issues: read a repository's issues through the Factory's…
AbirAbbas Nov 5, 2026
9912e01
Factory: group policies start released jobs inside an allowance and h…
AbirAbbas Nov 5, 2026
39dfeb1
factory.job_create: split_from records the job a new job was split fr…
AbirAbbas Nov 5, 2026
9b729fd
Merge app/v3 into workstreams/main
AbirAbbas Nov 5, 2026
0190d57
Factory: group policy, waits, split parents and outcome cursors read …
AbirAbbas Nov 5, 2026
e65dd2d
changes: entry for the Factory bookkeeping saved reads
AbirAbbas Nov 5, 2026
7a52835
Factory: check the saved reads at their own handlers
AbirAbbas Nov 6, 2026
86e2b4f
Factory: a transient GitHub or server failure stays retryable (#2439)
AbirAbbas Nov 6, 2026
ce5d561
session: an abandoned turn's paid steps survive a restart (#2440)
AbirAbbas Nov 6, 2026
a4b3079
Tasks: a worker does its own task, and a task tree is bounded in dept…
AbirAbbas Nov 6, 2026
a01e397
Merge app/v3 into workstreams/main
AbirAbbas Nov 6, 2026
331433e
Tasks: a proposed run reads only its assignment, workers add only und…
AbirAbbas Nov 6, 2026
cb06455
Merge app/v3 into workstreams/main
AbirAbbas Nov 6, 2026
c604eff
Factory: any GitHub 429 is a rate limit, never "repository not found"…
AbirAbbas Nov 6, 2026
c5c8ea1
Tasks: each check in a run gets its own checker ceiling (#2461)
AbirAbbas Nov 6, 2026
e985a44
A conversation can be told only about its own work (agents.set_scope)…
AbirAbbas Nov 6, 2026
e30414b
Tasks: the older task road refuses duplicate work and holds a tree sp…
AbirAbbas Nov 6, 2026
43615ed
App wire: announce collections.factory_job when collections hold Fact…
AbirAbbas Nov 6, 2026
631c78a
Models: a fallback model is for that run only, never the saved choice…
AbirAbbas Nov 6, 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
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ on:
- dev
- app/v3
- santos/dev2
- workstreams/main
# Also on the push, because a merge of two individually-green branches can
# still be red, and the merge is exactly the moment nobody is watching.
push:
branches:
- dev
- santos/dev2
- workstreams/main
workflow_dispatch:

permissions:
Expand Down
19 changes: 15 additions & 4 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ func runApp(args []string) error {
// ONE SUPERVISOR FOR EVERY CONVERSATION THE APP BUILDS (appservices.go).
services := openAppServices()
appServiceDoor.Store(services)
appClientToolsBackend.Store(backend)
backend.attachServices(services)
guard.Go("app/crews-start", func() { backend.crews() })
// THE APP IS AN OPEN WINDOW for as long as it runs, so the clock that runs
Expand Down Expand Up @@ -561,6 +562,7 @@ func appConnOf(client *remote.Client, release func(), assembled bool) *appConn {

// appBackend is the [appwire.Engine] over this process's conversations.
type appBackend struct {
clientTools appClientTools
libraryWatch sync.Once
libraryInboxMu sync.Mutex
pluginPauses map[string]appPluginPause
Expand Down Expand Up @@ -763,6 +765,7 @@ type appBackend struct {
collections *workspace.Store
collectionsClosed bool
collectionsPath string
collectionFactory collectionFactory
// services is the app's one supervisor of kept-running processes
// (appservices.go); nil is the capability absent.
services *appServices
Expand Down Expand Up @@ -1276,10 +1279,12 @@ func appRowOnDisk(id string) (session.SessionRow, bool) {
// A folder minted and never spoken in is still a conversation the app
// may have created; its meta names the workspace.
meta, err := session.LoadMeta(dir)
if err != nil || strings.TrimSpace(meta.ID) == "" {
if err != nil || meta.ID != id {
continue
}
return session.SessionRow{ID: id, AgentDID: meta.AgentDID(), Dir: dir, Transcript: transcript, Title: meta.Title, Workspace: meta.Workspace, Created: meta.Created}, true
return session.SessionRow{ID: id, AgentDID: meta.AgentDID(), Dir: dir, Transcript: transcript,
Title: meta.Title, Workspace: meta.Workspace, Owned: meta.Owned, Model: meta.Model,
Archived: meta.Archived, Spend: meta.SpentUSD, At: meta.Created, Created: meta.Created}, true
}
return session.SessionRow{}, false
}
Expand Down Expand Up @@ -1934,6 +1939,9 @@ func (b *appBackend) hold(a *appAgent, conn *appConn, workspace string) appwire.
if door, ok := conn.Conv.(interface{ ModelIdentity() session.ModelIdentity }); ok {
state := door.ModelIdentity()
modelIdentity = &state
if state.Selected != "" {
model = state.Selected
}
}
effort := conn.Conv.ReasoningFor(model)
if dial, ok := conn.Conv.(interface{ ConversationEffort() string }); ok {
Expand Down Expand Up @@ -1987,6 +1995,7 @@ func (b *appBackend) hold(a *appAgent, conn *appConn, workspace string) appwire.
a.crewHeld = a.crewHeld || crewHeld
view = a.view
})
b.holdClientTools(a, live)
startTitles()
b.flushSync(a)
b.locked(func() { view = a.view })
Expand Down Expand Up @@ -2918,7 +2927,7 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
b.stepMoved(a, live, title)
row := b.callBegan(a, id, turn, event, title)
row.Summary = event.Hint
live.box.Event(appwire.EventTurnTool, row)
live.sayTool(row)
case session.EventToolOutput:
b.callOutput(a, id, live, event)
case session.EventToolFinished:
Expand All @@ -2937,7 +2946,7 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
if title == "" {
title = redact.Secrets(tui3.ToolTitle(event.Tool, event.Args, ""))
}
live.box.Event(appwire.EventTurnTool, b.callEnded(a, id, turn, event, title, summary, appDetail(summary), succeeded))
live.sayTool(b.callEnded(a, id, turn, event, title, summary, appDetail(summary), succeeded))
b.noteContext(a, live)
// THE SEVENTEENTH PROCESS is refused by the engine, and the turn
// says so once at its end.
Expand Down Expand Up @@ -4572,6 +4581,8 @@ func (b *appBackend) Close() error {
if b.leaving != nil {
b.leaving()
}
b.clientTools.disconnect()
appClientToolsBackend.CompareAndSwap(b, nil)
already := false
var crews *appCrewRun
var agents []*appAgent
Expand Down
136 changes: 136 additions & 0 deletions cmd/codeaf/app_model_for_now_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

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

"github.com/Agent-Field/codeaf/internal/appwire"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/modelsource/sourcestub"
"github.com/Agent-Field/codeaf/internal/session"
)

// C1, C2, C3: the app reopens the saved choice and title route, leaving settings
// and setup state byte-identical across an automatic run-only selection.
func TestAppReopensConfiguredModelAfterRunOnlyReplacement(t *testing.T) {
t.Setenv(home.EnvVar, t.TempDir())
profile := t.TempDir()
t.Setenv(config.ProfileDirEnv, profile)
dir, workspace := t.TempDir(), t.TempDir()
path := filepath.Join(dir, "transcript.jsonl")
provider := sourcestub.New("vendor/configured", "available/model", "vendor/backup")
defer provider.Close()
settings := []byte(`{
"model.talk": "vendor/configured",
"models.tiers.low": "",
"models.tiers.reflex": "vendor/reflex",
"models.tiers.worker": "vendor/worker",
"models.tiers.high": "vendor/checker",
"models.tiers.mastermind": "vendor/planner",
"models.roles": "planner:vendor/planner",
"task.model": "vendor/task",
"models.tasks.mode": "auto",
"models.tasks.one": "vendor/one"
}
`)
if err := os.WriteFile(config.BudgetConfigPath(profile), settings, 0600); err != nil {
t.Fatal(err)
}
rolesSource, err := v3RolesSource(workspace, profile)
if err != nil {
t.Fatal(err)
}
cfg := session.Config{Workspace: workspace, Place: session.Place{Dir: dir, Workspace: workspace}, SessionFile: path,
Model: "vendor/configured", APIKey: "fixture", BaseURL: provider.URL(), ProfileDir: profile, System: "SYSTEM",
RolesSource: rolesSource,
ConversationModels: &session.ConversationModels{Main: "vendor/configured", Fallback: "vendor/backup"},
}
a, err := session.New(cfg)
if err != nil {
t.Fatal(err)
}
defer a.Close()
var reopened *session.Agent
b := newFakeBackend(newAppRecorder(), func(ask appOpenAsk) (*appConn, error) {
meta, err := session.LoadMeta(dir)
if err != nil {
return nil, err
}
fresh := cfg
fresh.Model = meta.Model
reopened, err = session.New(fresh)
if err != nil {
return nil, err
}
return &appConn{Conv: reopened, Workspace: workspace, Transcript: path, Release: func() { _ = reopened.Close() }}, nil
})
defer b.Close()
b.state = &appState{path: filepath.Join(t.TempDir(), appStateName)}
if err := b.state.write(func(f *appStateFile) error {
f.Setup = &appwire.Setup{DefaultModel: cfg.Model}
return nil
}); err != nil {
t.Fatal(err)
}
stateBefore, err := os.ReadFile(b.state.where())
if err != nil {
t.Fatal(err)
}
if err := a.UseModelForNow("available/model"); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
events, err := a.Submit(ctx, "respond on the available model")
if err != nil {
t.Fatal(err)
}
for event := range events {
if event.Kind == session.EventError {
t.Fatalf("scripted turn failed: %v", event.Err)
}
}
if err := a.Close(); err != nil {
t.Fatal(err)
}
a.SettleWrites()
profileAfter, err := os.ReadFile(config.BudgetConfigPath(profile))
if err != nil || !bytes.Equal(settings, profileAfter) {
t.Fatalf("fallback wrote profile: %s, %v", profileAfter, err)
}
stateAfter, err := os.ReadFile(b.state.where())
if err != nil || !bytes.Equal(stateBefore, stateAfter) {
t.Fatalf("fallback wrote app setup: %s, %v", stateAfter, err)
}
row, ok := session.ReadRows([]string{path})[filepath.Clean(path)]
if !ok {
t.Fatal("saved conversation is missing from app inventory")
}
listed := appRowView(row)
if listed.Model != cfg.Model || listed.ModelIdentity == nil || listed.ModelIdentity.Selected != cfg.Model {
t.Fatalf("agents.list row=%+v", listed)
}
b.remember(row)
opened, err := b.OpenAgent(listed.ID)
if err != nil || opened.Model != cfg.Model {
t.Fatalf("agents.open=%+v, %v", opened, err)
}
routes, _, err := b.errandRoutes(listed.ID)
if err != nil {
t.Fatal(err)
}
for _, route := range routes {
if route.Role == "title" {
if route.Model != cfg.Model {
t.Fatalf("agents.errand.models title=%+v", route)
}
return
}
}
t.Fatal("agents.errand.models omitted title")
}
2 changes: 1 addition & 1 deletion cmd/codeaf/appblank.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (b *appBackend) reapBlank(a *appAgent) bool {
}
gone := false
_ = teamstore.InspectConversation(b.crewProfile(), transcript, func(referenced bool) error {
if referenced || session.InUse(transcript) || !v3EmptyUnassignedSession(dir) {
if referenced || session.InUse(transcript) || !v3EmptyUnassignedSession(dir) || collectionKeepsConversation(dir) {
return nil
}
gone = os.RemoveAll(dir) == nil
Expand Down
Loading
Loading