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
161 changes: 142 additions & 19 deletions cmd/codeaf/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti
if _, err := fmt.Fprintln(output, judgeLastLine(last)); err != nil {
return err
}
// What is waiting for a judge and what the last sweep did are said
// beside it: the three lines together answer whether a run — a chat
// landing or one a headless door left — is being scored at all.
if _, err := fmt.Fprintln(output, pendingJudgeLine(readPendingJudge(poolDir), now)); err != nil {
return err
}
if _, err := fmt.Fprintln(output, sweepLastLine(readSweepLast(poolDir), now)); err != nil {
return err
}
relay, mirror := probePool(poolDir, cfg, now, keys)
if _, err := fmt.Fprintln(output, relayStatusLine(cfg, relay, mirror, cached)); err != nil {
return err
Expand All @@ -273,6 +282,76 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti
return nil
}

// pendingJudgeSummary is the pending file as status carries it: the rows no
// judged marker retires yet, and — when any wait — the oldest one's door and
// moment. OldestAt is nil when the oldest row was written before rows carried
// a moment, which the line says as an unknown age rather than inventing one.
type pendingJudgeSummary struct {
Count int `json:"count"`
OldestDoor string `json:"oldest_door,omitempty"`
OldestAt *time.Time `json:"oldest_at,omitempty"`
}

// readPendingJudge counts what the pending file is holding for the restart
// sweep: the rows a judged marker does not retire. A file that is missing, or
// a line that is torn, reads as the rows it does hold — the reading form
// reports what a person has. The oldest row is the earliest moment among
// them, and a row with no moment is the oldest there can be: it predates the
// stamp.
func readPendingJudge(poolDir string) pendingJudgeSummary {
var summary pendingJudgeSummary
data, err := os.ReadFile(pendingPath(poolDir))
if err != nil {
return summary
}
var oldestAt time.Time
var oldestDoor string
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var row pendingLanding
if json.Unmarshal([]byte(line), &row) != nil {
continue
}
if alreadyJudged(poolDir, row.Landing.ID, row.Landing.Attempt) {
continue
}
summary.Count++
door := row.Door
if door == "" {
door = "task"
}
if summary.Count == 1 || row.At.Before(oldestAt) {
oldestAt, oldestDoor = row.At, door
}
}
if summary.Count > 0 {
summary.OldestDoor = oldestDoor
if !oldestAt.IsZero() {
summary.OldestAt = &oldestAt
}
}
return summary
}

// pendingJudgeLine is the one line status says about the pending file: how
// many runs wait for a judge and the oldest one's door and age. A row written
// before rows carried a moment says an unknown age — it predates the stamp —
// and a file with nothing waiting is said in a sentence, the way every other
// nothing here is said.
func pendingJudgeLine(summary pendingJudgeSummary, now time.Time) string {
if summary.Count == 0 {
return "pending judge: none"
}
age := "age unknown"
if summary.OldestAt != nil {
age = reltime.Short(*summary.OldestAt, now)
}
return fmt.Sprintf("pending judge: %d · oldest %s run %s", summary.Count, summary.OldestDoor, age)
}

// poolProbeBudget is what status spends asking one address. It is short on
// purpose: the line is a reading a person waits for, and an address that takes
// longer than this to answer has not answered.
Expand Down Expand Up @@ -399,25 +478,27 @@ func ownSheetSummary(poolDir string) ownSummary {
}

// poolAnswer is the --json shape of the reading forms: the config flat, the
// cached index under index or null. The three status fields are pointers so a
// show carries none of them — a field a form does not answer reads as
// not-asked rather than as zero.
// cached index under index or null. The status fields are pointers so a show
// carries none of them — a field a form does not answer reads as not-asked
// rather than as zero.
type poolAnswer struct {
Mode string `json:"mode"`
ModeSource string `json:"mode_source"`
RelayURL string `json:"relay_url"`
IndexURL string `json:"index_url"`
MirrorURL string `json:"mirror_url"`
SubmitURL string `json:"submit_url"`
TTLSeconds int `json:"ttl_seconds"`
Pending *int `json:"pending,omitempty"`
CanSend *bool `json:"can_send,omitempty"`
CanRead *bool `json:"can_read,omitempty"`
Relay *probeSummary `json:"relay,omitempty"`
Mirror *probeSummary `json:"mirror,omitempty"`
LastJudge *judgeLast `json:"last_judge"`
Index *indexSummary `json:"index"`
Own ownSummary `json:"own"`
Mode string `json:"mode"`
ModeSource string `json:"mode_source"`
RelayURL string `json:"relay_url"`
IndexURL string `json:"index_url"`
MirrorURL string `json:"mirror_url"`
SubmitURL string `json:"submit_url"`
TTLSeconds int `json:"ttl_seconds"`
Pending *int `json:"pending,omitempty"`
PendingJudge *pendingJudgeSummary `json:"pending_judge,omitempty"`
CanSend *bool `json:"can_send,omitempty"`
CanRead *bool `json:"can_read,omitempty"`
Relay *probeSummary `json:"relay,omitempty"`
Mirror *probeSummary `json:"mirror,omitempty"`
LastJudge *judgeLast `json:"last_judge"`
LastSweep *sweepLast `json:"last_sweep"`
Index *indexSummary `json:"index"`
Own ownSummary `json:"own"`
}

// indexSummary is the cached index as the reading forms carry it: the
Expand Down Expand Up @@ -492,16 +573,21 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached
answer.Pending = &pending
answer.CanSend = &send
answer.CanRead = &read
judged := readPendingJudge(poolDir)
answer.PendingJudge = &judged
relay, mirror := probePool(poolDir, cfg, now, keys)
answer.Relay = &relay
answer.Mirror = &mirror
}
answer.Own = ownSheetSummary(poolDir)
// The record is null when there is none, so a script can tell a judge
// The records are null when there is none, so a script can tell a judge
// that has not run from one that failed.
if last := readJudgeLast(poolDir); last != nil {
answer.LastJudge = last
}
if swept := readSweepLast(poolDir); swept != nil {
answer.LastSweep = swept
}
encoded, err := json.Marshal(answer)
if err != nil {
return err
Expand Down Expand Up @@ -632,6 +718,22 @@ func readJudgeLast(poolDir string) *judgeLast {
return &last
}

// readSweepLast reads what the sweep left about its own run: what it judged,
// what its budget left waiting, and whether the deadline ended it. A file
// that is missing, or one that does not parse, reads as none yet — the
// reading form reports what a person has.
func readSweepLast(poolDir string) *sweepLast {
data, err := os.ReadFile(filepath.Join(poolDir, "sweep-last.json"))
if err != nil {
return nil
}
var last sweepLast
if json.Unmarshal(data, &last) != nil {
return nil
}
return &last
}

// judgeLastLine is the one line status says about the last judge: which model
// answered and which seats it scored, or — when none did — how many were
// asked, the first of them, and the one-line reason the last one failed. The
Expand All @@ -654,6 +756,27 @@ func judgeLastLine(last *judgeLast) string {
return fmt.Sprintf("last judge: %s · failed after %d candidates (%s) · %s", at, len(last.Tried), asked, last.Reason)
}

// sweepLastLine is the one line status says about the last sweep: what it
// judged, what it left waiting when the deadline cut it, and how much of the
// sweep's own budget it spent. The moment is the record's own, said the way
// the surface says every when — relatively. A record with no moment is no
// record worth reporting, the same reading a missing file takes.
func sweepLastLine(last *sweepLast, now time.Time) string {
if last == nil || last.At.IsZero() {
return "last sweep: none yet"
}
parts := []string{
reltime.Short(last.At, now) + " ago",
fmt.Sprintf("judged %d", last.Judged),
}
if last.Left > 0 {
parts = append(parts, fmt.Sprintf("%d still pending", last.Left))
}
parts = append(parts, fmt.Sprintf("%s of %s",
reltime.Elapsed(time.Duration(last.BudgetUsed)*time.Second), reltime.Elapsed(poolSweepBudget)))
return "last sweep: " + strings.Join(parts, " · ")
}

// pendingRows counts what the outbox is holding. It reads the file by count
// and not by opening it, because [outbox.Open] CREATES the file when it is
// not there and a reading form must not write.
Expand Down
170 changes: 170 additions & 0 deletions cmd/codeaf/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,176 @@ func TestPoolStatusJSONCarriesTheLastJudge(t *testing.T) {
}
}

// seedPendingRows writes pending rows the way the doors leave them, by hand
// rather than through writePendingLanding: the writer stamps the moment
// itself, and a status test needs to hold the ages still.
func seedPendingRows(t *testing.T, dir string, rows ...pendingLanding) {
t.Helper()
poolDir := filepath.Join(dir, "pool")
if err := os.MkdirAll(poolDir, 0o755); err != nil {
t.Fatal(err)
}
var body strings.Builder
for _, row := range rows {
data, err := json.Marshal(row)
if err != nil {
t.Fatal(err)
}
body.Write(data)
body.WriteByte('\n')
}
if err := os.WriteFile(filepath.Join(poolDir, "pending.jsonl"), []byte(body.String()), 0o600); err != nil {
t.Fatal(err)
}
}

// seedJudgedMarker lays down the marker the judge leaves for a run it scored,
// so status reads the row behind it as judged.
func seedJudgedMarker(t *testing.T, dir string, id uint64, attempt int) {
t.Helper()
poolDir := filepath.Join(dir, "pool")
if err := os.MkdirAll(judgedDir(poolDir), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(judgedMarkerPath(poolDir, id, attempt), []byte{}, 0o600); err != nil {
t.Fatal(err)
}
}

// seedSweepLast writes the sweep's record the way the sweep leaves it, so
// status is read against what stands on disk.
func seedSweepLast(t *testing.T, dir string, last sweepLast) {
t.Helper()
data, err := json.Marshal(last)
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dir, "pool"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "pool", "sweep-last.json"), data, 0o600); err != nil {
t.Fatal(err)
}
}

// status says what is waiting for a judge and what the last sweep did: the
// pending file's unjudged rows with the oldest one's door and age, and the
// sweep's own record of what it judged, what its budget left and whether the
// deadline cut it. --json carries both records, and a row written before rows
// carried a moment says an unknown age rather than inventing one.
func TestPoolStatusCountsThePendingJudgeRowsAndSaysWhatTheLastSweepDid(t *testing.T) {
dir := t.TempDir()
do := poolTestLanding()
do.ID, do.Attempt = 7, 1
exec := poolTestLanding()
exec.ID, exec.Attempt = 9, 2
threeHoursAgo := poolClock(t)().Add(-3 * time.Hour)
seedPendingRows(t, dir,
pendingLanding{At: threeHoursAgo, Door: "do", Landing: do},
pendingLanding{Door: "exec", Landing: exec},
)
seedJudgedMarker(t, dir, 9, 2)
seedSweepLast(t, dir, sweepLast{
At: poolClock(t)().Add(-2 * time.Minute), Judged: 3, Left: 2,
BudgetUsed: 41, Cut: true,
})

var out strings.Builder
if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), deadEnv()); err != nil {
t.Fatal(err)
}
body := out.String()
if !strings.Contains(body, "pending judge: 1 · oldest do run 3h") {
t.Fatalf("status did not count the waiting rows:\n%s", body)
}
if !strings.Contains(body, "last sweep: 2m ago · judged 3 · 2 still pending · 41s of 10m") {
t.Fatalf("status did not say what the last sweep did:\n%s", body)
}

out.Reset()
if err := runPoolWith([]string{"status", "--json"}, &out, dir, poolClock(t), deadEnv()); err != nil {
t.Fatal(err)
}
var answer struct {
PendingJudge *struct {
Count int `json:"count"`
OldestDoor string `json:"oldest_door"`
OldestAt *time.Time `json:"oldest_at"`
} `json:"pending_judge"`
LastSweep *struct {
At time.Time `json:"at"`
Judged int `json:"judged"`
Left int `json:"left"`
BudgetUsed int `json:"budget_used"`
Cut bool `json:"cut"`
} `json:"last_sweep"`
}
if err := json.Unmarshal([]byte(out.String()), &answer); err != nil {
t.Fatalf("status --json did not parse: %v\n%s", err, out.String())
}
if answer.PendingJudge == nil || answer.PendingJudge.Count != 1 || answer.PendingJudge.OldestDoor != "do" {
t.Fatalf("the waiting rows did not carry: %+v", answer.PendingJudge)
}
if got, err := time.Parse(time.RFC3339, "2026-09-17T21:00:00Z"); err != nil ||
answer.PendingJudge.OldestAt == nil || !answer.PendingJudge.OldestAt.Equal(got) {
t.Fatalf("the oldest row's moment is %v, want the row's own in RFC 3339", answer.PendingJudge.OldestAt)
}
if answer.LastSweep == nil || answer.LastSweep.Judged != 3 || answer.LastSweep.Left != 2 ||
answer.LastSweep.BudgetUsed != 41 || !answer.LastSweep.Cut {
t.Fatalf("the sweep's record did not carry: %+v", answer.LastSweep)
}
if got, err := time.Parse(time.RFC3339, "2026-09-17T23:58:00Z"); err != nil || !answer.LastSweep.At.Equal(got) {
t.Fatalf("the sweep's moment is %v, want the record's own in RFC 3339", answer.LastSweep.At)
}

// A row written before rows carried a moment is the oldest of them — it
// predates the stamp — and says so rather than inventing an age.
unknown := t.TempDir()
seedPendingRows(t, unknown,
pendingLanding{At: threeHoursAgo, Door: "do", Landing: do},
pendingLanding{Door: "exec", Landing: exec},
)
out.Reset()
if err := runPoolWith([]string{"status"}, &out, unknown, poolClock(t), deadEnv()); err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), "pending judge: 2 · oldest exec run age unknown") {
t.Fatalf("status did not say the oldest row's age is unknown:\n%s", out.String())
}
}

// An install nothing has reached — no pending file, no sweep record — says so
// in the two sentences a nothing is said in here, and the reading form writes
// nothing while it looks.
func TestPoolStatusSaysWhenNothingWaitsAndNoSweepHasRun(t *testing.T) {
quiet := t.TempDir()
var out strings.Builder
if err := runPoolWith([]string{"status"}, &out, quiet, poolClock(t), deadEnv()); err != nil {
t.Fatal(err)
}
body := out.String()
if !strings.Contains(body, "pending judge: none") {
t.Fatalf("status did not say nothing waits:\n%s", body)
}
if !strings.Contains(body, "last sweep: none yet") {
t.Fatalf("status did not say no sweep has run:\n%s", body)
}
if _, err := os.Stat(filepath.Join(quiet, "pool", "pending.jsonl")); !os.IsNotExist(err) {
t.Fatal("status created the pending file it was only counting")
}

out.Reset()
if err := runPoolWith([]string{"status", "--json"}, &out, quiet, poolClock(t), deadEnv()); err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), `"pending_judge":{"count":0}`) {
t.Fatalf("an absent pending file did not read as zero:\n%s", out.String())
}
if !strings.Contains(out.String(), `"last_sweep":null`) {
t.Fatalf("a missing sweep record was not said as null:\n%s", out.String())
}
}

// An empty profile is the state root's own profile, the way every other file
// under the profile resolves — never a directory called "pool" beside wherever
// the command happened to run.
Expand Down
Loading