Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions cmd/codeaf/app_take_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ func appTakeRoundTrip(t *testing.T, daemon bool, missingWorkspace ...bool) {
Agent appwire.Agent `json:"agent"`
}](t, a.w, "agents.create", map[string]any{"workspace": workspace}).Agent
e2Turn(t, a, created.ID, "first")
heldFence := func() uint64 {
rows := machineCall[appwire.ChatsResult](t, a.w, "chats.list", struct{}{})
for _, r := range rows.Chats {
if r.Cell == created.ID && r.Here {
return r.Seen.Fence
}
}
return 0
}
var firstFence uint64
if !absent {
appSyncWait(t, "A's first published fence", func() bool {
firstFence = heldFence()
return firstFence > 0
})
}
// Start continuation from a chat saved by the real app leaving path. This
// exercises a saved chat independently of background first-publication timing.
e2Quit(t, a)
Expand All @@ -332,6 +348,11 @@ func appTakeRoundTrip(t *testing.T, daemon bool, missingWorkspace ...bool) {
} else {
a = e2StartApp(t, dirA, daemon)
machineCall[appwire.TakeResult](t, a.w, "agents.open", appwire.IDParams{ID: created.ID})
// Reopening owns the journal before its background drive acquires.
// Take consent must name that new hold rather than race its arrival.
appSyncWait(t, "A's reopened chat holds a new fence", func() bool {
return heldFence() > firstFence
})
}

t.Logf("created %s", string(e2JSON(created)))
Expand Down
29 changes: 24 additions & 5 deletions cmd/codeaf/appsync_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,29 @@ func TestC6AppSendDoesNotWaitForSyncStartupOrCapture(t *testing.T) {
entered, release := make(chan struct{}), make(chan struct{})
var once sync.Once
unblock := func() { once.Do(func() { close(release) }) }
var requests atomic.Int64
var requestMu sync.Mutex
var requestPaths []string
recordRequest := func(r *http.Request) {
requestMu.Lock()
defer requestMu.Unlock()
requestPaths = append(requestPaths, r.Method+" "+r.URL.Path)
}
requests := func() []string {
requestMu.Lock()
defer requestMu.Unlock()
return append([]string(nil), requestPaths...)
}
var first sync.Once
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
// The drive's startup opens with a device listing, at the same
// moment as the app's own chat listing and watch. Every request
// is held, so a startup held at its listing cannot reach the
// chat's cell; the listings themselves are not admission's work
// and arrive in either order, so they are not counted.
listing := r.Method == http.MethodGet && (r.URL.Path == "/v1/dir/list" || r.URL.Path == "/v1/dir/watch")
if boundary != "startup" || !listing {
recordRequest(r)
}
if boundary == "startup" {
first.Do(func() { close(entered) })
select {
Expand Down Expand Up @@ -120,7 +139,7 @@ func TestC6AppSendDoesNotWaitForSyncStartupOrCapture(t *testing.T) {
}
})
}
before := requests.Load()
before := len(requests())
type sent struct {
turn uint64
err error
Expand All @@ -145,8 +164,8 @@ func TestC6AppSendDoesNotWaitForSyncStartupOrCapture(t *testing.T) {
case <-time.After(10 * time.Second):
t.Fatal("the admitted send never reached the real session model")
}
if got := requests.Load(); got != before {
t.Fatalf("admission added relay work: before %d, after %d", before, got)
if got := requests(); len(got) != before {
t.Fatalf("admission added relay work: before %d, after %d; added %v", before, len(got), got[before:])
}
unblock()
releaseModel()
Expand Down
11 changes: 5 additions & 6 deletions cmd/codeaf/chatv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1473,7 +1473,8 @@ func openV3Agent(cfg session.Config, workspace string, open func(session.Config)
if model != "" {
cfg.Model = model
}
cfg = v3Seated(v3Migrated(cfg))
cfg, claim := v3SeatedPending(v3Migrated(cfg))
defer claim(false)
// Restore the gate before construction, so restored work cannot start behind
// the profile default. Return the launch config unchanged: a subsequent new
// conversation must not inherit this one's saved override.
Expand Down Expand Up @@ -1505,6 +1506,7 @@ func openV3Agent(cfg session.Config, workspace string, open func(session.Config)
}
agent, err := open(restored)
if err == nil {
claim(true)
if model != "" {
if err := agent.ContinueWithModel(model, modelNotice); err != nil {
closeV3Agent(agent)
Expand Down Expand Up @@ -1579,11 +1581,8 @@ func v3TakeOverInstead(cfg session.Config, workspace string) (*session.Agent, se
if err != nil {
return nil, cfg, err
}
agent, err := v3OpenSession(fresh)
if err != nil {
return nil, cfg, err
}
return agent, fresh, nil
agent, fresh, _, err := openV3Agent(fresh, workspace, v3OpenSession)
return agent, fresh, err
}

// sessionHeldElsewhereSentence is what a person reads when the conversation
Expand Down
30 changes: 22 additions & 8 deletions cmd/codeaf/chatv3_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,16 +787,23 @@ func v3Migrated(cfg session.Config) session.Config {
return cfg
}

// v3Seated gives the session the executor it owns, built once here from the
// class the cell declared and the workspace its tools run in.
// v3Seated is for a caller that already holds the journal. It gives the session
// the executor it owns, built from the cell class and its tools' workspace.
//
// A folder that is not a readable cell gets no seat and its calls run on the
// host as they always did, and so does every folder without a compatible engine. The
// seat is set on every call, so a config reused for the next conversation never
// carries the last one's executor.
func v3Seated(cfg session.Config) session.Config {
cfg, claim := v3SeatedPending(cfg)
claim(true)
return cfg
}

// v3SeatedPending builds the seat before committing its drive to journal ownership.
func v3SeatedPending(cfg session.Config) (session.Config, func(bool)) {
cfg.Seat, cfg.Machine, cfg.Seals, cfg.Interrupted = nil, nil, nil, nil
seat, machine, seals := v3SeatWith(cfg.Place, nil, syncDrives)
seat, machine, seals, claim := v3SeatPending(cfg.Place, nil, syncDrives)
cfg.Seat, cfg.Interrupted = seat, executor.InterruptedOn(seat)
cfg.Renewals = syncDrives.renewalsOf(cfg.Place.ID())
if machine != nil {
Expand All @@ -805,7 +812,7 @@ func v3Seated(cfg session.Config) session.Config {
if seals != nil {
cfg.Seals = seals
}
return cfg
return cfg, claim
}

// v3SeatOf is the seat of the cell a place names, the machine view it was
Expand All @@ -822,12 +829,19 @@ func v3SeatOf(place session.Place, say func(string)) (executor.Seat, *preflight.
// the chat its tool calls are refused. A nil book, or sync off, is the seat
// exactly as it was.
func v3SeatWith(place session.Place, say func(string), drives *driveBook) (executor.Seat, *preflight.Machine, *cellstore.SealWatch) {
seat, machine, watch, claim := v3SeatPending(place, say, drives)
claim(true)
return seat, machine, watch
}

func v3SeatPending(place session.Place, say func(string), drives *driveBook) (executor.Seat, *preflight.Machine, *cellstore.SealWatch, func(bool)) {
noop := func(bool) {}
if !cell.Available() || place.Dir == "" || place.Workspace == "" {
return nil, nil, nil
return nil, nil, nil, noop
}
c, err := cell.OpenAt(place.Dir, filepath.Base(place.Dir))
if err != nil {
return nil, nil, nil
return nil, nil, nil, noop
}
class, ok := executor.ParseClass(string(c.Meta().Class))
if !ok {
Expand All @@ -836,7 +850,7 @@ func v3SeatWith(place session.Place, say func(string), drives *driveBook) (execu
machine, err := preflight.OpenMachine(c.Root, place.Workspace)
watch := &cellstore.SealWatch{Say: say}
failed(watch, err)
drive := drives.driveOf(c, cellstore.EngineFor(place.Workspace), watch.Report)
drive, claim := drives.pendingDriveOf(c, cellstore.EngineFor(place.Workspace), watch.Report)
if drive != nil {
watch.OnTurnEnd = drive.Idle // the agent waits for the person: upload what is sealed now
}
Expand All @@ -847,7 +861,7 @@ func v3SeatWith(place session.Place, say func(string), drives *driveBook) (execu
// 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), driveWorkGate(drive)), machine, watch
return executor.Gated(seat, driveGate(drive), driveWorkGate(drive)), machine, watch, claim
}

// sealTail seals what the chat wrote after the turn-end seal (titles, summaries,
Expand Down
4 changes: 2 additions & 2 deletions cmd/codeaf/chatv3_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func TestALaunchThatMeetsALockOpensAFreshOneAndCarriesTheHeldRow(t *testing.T) {
}
defer func() { _ = first.Close() }()

_, _, _, err = openV3Agent(cfg, workspace, v3OpenSession)
_, seated, _, err := openV3Agent(cfg, workspace, v3OpenSession)
// THE FAILURE IS TYPED AND CARRIES THE PATH, because the sentence
// deliberately does not contain one and the surface needs one to point at.
var held *sessionHeldElsewhere
Expand All @@ -159,7 +159,7 @@ func TestALaunchThatMeetsALockOpensAFreshOneAndCarriesTheHeldRow(t *testing.T) {
t.Fatalf("%v no longer reads as a locked session", err)
}

second, fresh, err := v3TakeOverInstead(cfg, workspace)
second, fresh, err := v3TakeOverInstead(seated, workspace)
if err != nil {
t.Fatalf("the second window was left with nothing: %v", err)
}
Expand Down
Loading