Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
caa6d39
plandb: check terminal status before ownership check in Done, prune s…
agentfield-bot Sep 27, 2026
c82483b
session: fall back to project place workspace when workspace is non-r…
agentfield-bot Sep 27, 2026
2653fdd
docs(changes): add changelog entry for PR 1562
agentfield-bot Sep 27, 2026
f6aca9b
prompts: restore bashworker prompt to keep prompt size under 16 KiB
agentfield-bot Sep 27, 2026
07e77ac
headless: a blank brief is refused before any work starts
santoshkumarradha Sep 27, 2026
38861e3
delegate: flags after the brief are parsed, unknown ones refused
santoshkumarradha Sep 27, 2026
0e07534
headless: do --json carries the run engine's token counts
santoshkumarradha Sep 27, 2026
6b107f8
chat: esc cancels a pending browser sign-in
santoshkumarradha Sep 27, 2026
0c897b4
session: a read hand-off helper is read-only and leaves no stopped card
santoshkumarradha Sep 27, 2026
e284e0d
session: plan-born task workers carry the standing orders
santoshkumarradha Sep 27, 2026
a6f91b8
standing: project orders match a cleaned path, cards quote the live a…
santoshkumarradha Sep 27, 2026
66475ea
chat: an empty /drafts or /skill never traps the keyboard
santoshkumarradha Sep 27, 2026
12b2f7d
teams: managers are addressable at once, sub-teams keep the posture, …
santoshkumarradha Sep 27, 2026
7e15975
jobs: bound the disk spool and let the footer admit the truncation
agentfield-bot Sep 27, 2026
ab6f4d0
session: a machine-held task owns no folder, stops at once, and a sto…
santoshkumarradha Sep 27, 2026
c55a77d
fix(jobs): preserve spool identity and report incomplete output honestly
agentfield-bot Sep 27, 2026
0f5fcc4
media: pictures are named by their bytes, and speech spend reaches th…
santoshkumarradha Sep 27, 2026
fe03d00
fix(jobs): retain bounded completed logs with cross-process ownership
agentfield-bot Sep 27, 2026
d16eabd
fix(jobs): finish integrating retention and task log diagnostics
agentfield-bot Sep 27, 2026
59eaa7b
fix(jobs): preserve registry identity across workspace changes
agentfield-bot Sep 27, 2026
224dd49
docs(jobs): explain unsafe log storage refusal
agentfield-bot Sep 27, 2026
b174e62
fix(jobs): preserve safe startup expiry for completed log payloads
agentfield-bot Sep 27, 2026
62b2942
fix: exclude runtime output from recursive search and bound log reads
agentfield-bot Sep 27, 2026
97b04ee
docs: consolidate bounded runtime logs change entry
agentfield-bot Sep 27, 2026
8690d9b
docs: use plain language for retained log ownership
agentfield-bot Sep 27, 2026
16a4644
docs: make completed log retention separately searchable
agentfield-bot Sep 27, 2026
659383d
fix: preserve grep matches when bounded context skips long lines
agentfield-bot Sep 27, 2026
f732247
docs: associate bounded logs change with PR 1603
agentfield-bot Sep 27, 2026
b22359d
fix: keep runtime search guidance within prompt budgets
agentfield-bot Sep 27, 2026
c847d9e
remote: a redial's ssh stderr stays out of the full-screen frame
santoshkumarradha Sep 27, 2026
a6b558c
chat: the / list names every argument form, /compact speaks plainly, …
santoshkumarradha Sep 27, 2026
121be16
remote: a redialing window takes its own keyboard back, and only its own
santoshkumarradha Sep 27, 2026
01ba6f9
runs: each check has its own ceiling, unfinished checks fail the run,…
santoshkumarradha Sep 27, 2026
27f8511
home: a conversation whose engine answers for another project is refu…
santoshkumarradha Sep 27, 2026
8d6c4c5
wall: shows only this window's conversations, and a tile opened from …
santoshkumarradha Sep 27, 2026
2e7a862
chat: angle brackets survive, team names paint, Lyria composes, hoste…
santoshkumarradha Sep 27, 2026
3fb3b04
changes: the docs-audit batch's change entry
santoshkumarradha Sep 27, 2026
5c0512c
changes: close the batch entry's frontmatter and shorten its title
santoshkumarradha Sep 27, 2026
8254cb0
session: take back the restart reconcile of an interrupted run's plan
santoshkumarradha Sep 27, 2026
0b86a5f
session: take back the part-check path refusal
santoshkumarradha Sep 27, 2026
4643131
changes: drop the part-check line from the batch entry
santoshkumarradha Sep 27, 2026
53945ab
standing: a firing that only reported a sentence comes to said, not l…
santoshkumarradha Sep 27, 2026
fe8fc0e
senior-dev: an unsubmitted run with passing checks says so and gives …
santoshkumarradha Sep 27, 2026
5b32e39
teams: the default daily cap is each team's own, and All teams has no…
santoshkumarradha Sep 27, 2026
7ab5896
Merge remote-tracking branch 'origin/fix/1599-bounded-job-logs' into …
santoshkumarradha Sep 27, 2026
ce6b1ce
Merge remote-tracking branch 'origin/fix/docs-audit-batch' into codex…
santoshkumarradha Sep 27, 2026
828d58b
test: allow one explicit live verification model across roles
santoshkumarradha Sep 27, 2026
6b32659
test: pin every terminal model role and expose default launch
santoshkumarradha Sep 27, 2026
dc0db14
fix: resolve project ground on the default run door
santoshkumarradha Sep 27, 2026
a2af60f
fix: tell every plan worker its assigned working directory
santoshkumarradha Sep 27, 2026
8a1d658
fix: bound foreground spills and preserve selected folder aliases
santoshkumarradha Sep 27, 2026
ee60b1e
fix: resume planned tasks with durable ground and reachable stop
santoshkumarradha Sep 27, 2026
e9bf4ce
fix: resume planned tasks with durable ground and reachable stop
santoshkumarradha Sep 27, 2026
61b9312
Merge branch 'codex/pr-live-integration' into codex/pr1604-live-review
santoshkumarradha Sep 27, 2026
ba79976
fix: exclude prompt history and pin every live text role
santoshkumarradha Sep 27, 2026
830d4c2
docs: align foreground change entry with pull request number
santoshkumarradha Sep 27, 2026
d638447
Merge branch 'codex/pr-live-integration' into codex/pr1604-live-review
santoshkumarradha Sep 27, 2026
f8c47b3
provider: a 401 or 403 is an auth failure everywhere and names the ke…
santoshkumarradha Sep 27, 2026
364510f
tasks: a kept branch says why, and /land lists a single task's kept b…
santoshkumarradha Sep 27, 2026
88db9fc
tasks: a held task says machine busy, stops from the main box, follow…
santoshkumarradha Sep 27, 2026
3ddb57c
spend: the daily limit holds the chat and chat-started tasks too
santoshkumarradha Sep 27, 2026
cbd0041
chat: a key the settle guard drops leaves a countdown card exactly as…
santoshkumarradha Sep 27, 2026
a6b4e2d
session: publishRunRow carries a run's kept facts through one helper
santoshkumarradha Sep 27, 2026
fc4374f
refactor: separate task restart lifecycle phases
santoshkumarradha Sep 27, 2026
4f3084e
provider: the auth failure names its key source on the default road, …
santoshkumarradha Sep 27, 2026
7f58260
merge: preserve concurrent audit fixes with durable task recovery
santoshkumarradha Sep 27, 2026
2a2fc4c
test: wait for run teardown and isolate heartbeat phase checks
santoshkumarradha Sep 27, 2026
b4965b2
fix: read resumed joined tasks through their live owner
santoshkumarradha Sep 27, 2026
153eb04
tasks: task.max_load changes reach the engine process, and an interru…
santoshkumarradha Sep 27, 2026
5a9e6fb
spend: a chat task past the daily limit asks first, and the top bar s…
santoshkumarradha Sep 27, 2026
34cff27
land: /land reaches the engine over the local host, so a kept task br…
santoshkumarradha Sep 27, 2026
5d0e793
fix: reconcile joined task lifecycle on admission and recovery
santoshkumarradha Sep 27, 2026
714b92c
test: observe delivered child news after conversation wake
santoshkumarradha Sep 27, 2026
3ee9076
merge: preserve latest audit admission and landing fixes
santoshkumarradha Sep 27, 2026
cbe91c6
fix: refresh admission settings in the default profile
santoshkumarradha Sep 27, 2026
bd589a0
fix: preserve program receipts and typed endings during recovery
santoshkumarradha Sep 27, 2026
e84b4e1
test: classify report-only divided firing as said
santoshkumarradha Sep 27, 2026
3a72e23
fix: settle cancelled budget holds and carry run spending permission
santoshkumarradha Sep 27, 2026
e3dd145
fix: apply current admission settings when a run is created
santoshkumarradha Sep 27, 2026
e556a5b
test: wait for program run owner before fixture cleanup
santoshkumarradha Sep 27, 2026
80aff6b
docs: describe recovered task states and finished rail accurately
santoshkumarradha Sep 27, 2026
0b56c71
test(session): synchronize completion claims with their readings
santoshkumarradha Sep 27, 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
96 changes: 96 additions & 0 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,32 @@ only omit what the reader is already holding. On the inherited-brief road the
address rides the prerequisite's HEADER, which the shared pot above does not
clip. Pinned by `internal/session/task_result_e2e_test.go`.

## A background job's disk spool is bounded

`internal/session/jobs.go` used to spool everything a background job wrote to
one `<id>.log` with no ceiling: a watcher printing for a week filled the disk
at whatever rate it printed, and the in-memory ring appended one Write before
trimming, so a single multi-megabyte Write grew a temporary to match.

The spool is now a window of at most **jobSpoolChunks (2) chunks of
jobSpoolChunkBytes (4MB)** — `<id>.log` live and `<id>.log.1` kept — rotated by
copying a full chunk once at its boundary, then truncating and seeking the
same live inode. The previous backup is removed before copying, keeping even
transient usage within two chunks. No job ID or writer lock is released during
rotation. One huge Write spools in chunk-sized pieces and hands
only its newest **64KB** (`jobRingBytes`) to the ring. The retained output
stays addressable by the read tool exactly as before, so no limit grows for
the reader.

The honesty is the point, and it is pinned: a spool that has discarded
anything — or a spool write, short write or close that failed — sets the
sink's notice, and every footer a model reads stops saying `full log:` and
names the truncation or the failure beside the file instead
(`TestJobFooterNamesTruncationInsteadOfFullLog`); rotation, the discard, the
huge-Write tail and the injected failure are pinned by
`internal/session/jobspool_test.go`. The bound is a fact about the code, not
about the box: the window is fixed bytes per job, not a disk-filling rate.

## Specialist tool discovery

Chat starts with core tools and one local `load_capability` registry operation
Expand Down Expand Up @@ -2615,3 +2641,73 @@ Rendering selects a phrase by elapsed ten-second interval and samples the existi
decoding ripple with 240 ms letter steps and a 1.8-second pause per pass. The 28-column caption and
nine-column mark have fixed widths. This uses the existing clock and one
foreground span; it adds no timer, I/O, model call or per-frame randomness.

## Completed job log retention

`internal/session/jobretention.go` limits eligible completed managed spools in
one jobs directory to **128 MiB and 64 job groups**, counting the base and
rotation together. Active spools have separate per-job limits; unmarked legacy
logs and unsafe files remain outside the budget because older writers may not
hold leases. This is not a machine-wide bound. Startup retains the existing
seven-day (`sweepTTL`) expiry for eligible inactive groups only: both chunks
must be older than the cutoff. Expired groups are removed before applying the
byte/count budget to fresh groups; metadata and active/legacy logs do not expire.

Maintenance runs at log creation, sink close, and the existing startup sweep,
never per output write. It
lists one jobs directory, sorts candidates by allocated ID, and takes
nonblocking independent file leases; a deletion holds its lease through unlink.
Directory locks serialize allocation and maintenance across processes. Counter
and ownership metadata reads are capped at 256 bytes. ID allocation persists a
high-water value before cleanup and keeps the writer lease before publishing
the marker, preventing both reused IDs and newborn-log eviction. The stable
lock file remembers initialization if a counter later disappears.

A claim with damaged metadata fails explicitly. Cleanup failures remain
retryable and are surfaced in the sink notice. Existing journals, worktrees,
legacy logs, and unrelated directories are not retention candidates. Tests use
explicit byte/count budgets with small payloads, avoiding mutable global limits.

The startup TTL sweeper delegates `logs/jobs/` to this retention instead of
expiring the stable allocation metadata or ownership markers by age.
## Runtime-safe file search

The structured `grep` tool excludes known codeaf runtime output for both engines,
including custom state homes and searches starting inside those directories.
The policy preserves source under `work/`, `trees/`, and ordinary user `logs/`
directories. Ripgrep receives exclusions after user globs and runs without user
config or symlink traversal. Shell commands do not inherit these protections.

### Foreground bash snapshots

Foreground bash retains at most **8 MiB** of initial output per spill under the
state home's `logs/bash/`, separately from its bounded latest-result tail.
Completed snapshots share **128 MiB / 64 files** and expire after **seven days**;
retention runs at creation and close. Active writers hold independent leases
and are outside the completed-file budget. A directory lock serializes cleanup
and publication. Unknown and unsafe linked files, and old `pi-bash-*.log`
temporary files, remain outside retention. These are per-directory bounds.

Write and close failures stop spooling, not draining. Incomplete output never
claims to be full. Promotion closes the foreground snapshot and sends later
bytes only to the job sink. Structured recursive search excludes new snapshot
directories and legacy pi-bash files in the current temporary directory.

`internal/exec/bare/bash_spill_test.go` covers the byte bound, initial-prefix
preservation, real foreground shell output, disk failures, promotion, retention
budgets, expiry, cross-process active leases, and linked-path refusal.

Recursive search skips files above **8 MiB**. The walking engine and explicit
single-file inspection read a snapshot bounded by `min(size-at-open, 8 MiB)`;
one **64 KiB** line buffer drains and skips oversized lines, with an incomplete
result notice. Stored matches are clipped to the existing 500-byte display cap,
with at most **1000 matches** and **20 context lines per side**. Context rendering
streams the same bounded snapshot reader and stops accumulating output once the
result byte budget has been reached. Directory traversal and total files searched
remain governed by the caller's context, rather than a machine-wide byte cap.

Ripgrep JSON records are limited to **1 MiB**; scanner errors and match limits
kill and reap the child so `Wait` cannot hang behind a full stdout pipe. Stderr
capture retains at most **4 KiB** while continuing to drain. Regression fixtures
cover both engines, direct runtime roots, aliases, broad globs, subprocess
termination, source worktrees, and growth during a snapshot read.
10 changes: 10 additions & 0 deletions cmd/codeaf/brief_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,13 @@ func TestABriefMayArriveOnStandardInput(t *testing.T) {
t.Fatalf("brief = %q, want the piped text", text)
}
}

func TestReadTextRejectsAnAllBlankArgumentList(t *testing.T) {
want := noGoalGiven("do").Error()
for _, args := range [][]string{{""}, {" "}, {"", "\t"}} {
text, err := readText("do", args)
if text != "" || err == nil || err.Error() != want {
t.Errorf("readText(%q) = %q, %v; want noGoalGiven", args, text, err)
}
}
}
13 changes: 7 additions & 6 deletions cmd/codeaf/carried.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,13 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
// name it has, so its rows are filed as one piece of work under it.
subject := filepath.Base(record)
api, err := modelapi.Open(modelapi.Config{
TaskDir: record,
CompleterFor: road.completerFor,
Serves: road.serves,
ModelPrice: road.modelPrice,
Seat: road.seat,
Ceiling: inv.Ceilings.CostUSD,
TaskDir: record,
CompleterFor: road.completerFor,
AuthKeySource: config.APIKeySourceAt(config.ProfileDir()),
Serves: road.serves,
ModelPrice: road.modelPrice,
Seat: road.seat,
Ceiling: inv.Ceilings.CostUSD,
Bank: func(charge modelapi.Charge) {
// THE MACHINE'S SPENDING LEDGER, one row per call, written here and
// nowhere else: nothing else in this process meters these calls.
Expand Down
47 changes: 39 additions & 8 deletions cmd/codeaf/chatv3_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (l *engineLink) spawn() (io.ReadWriteCloser, error) {
// is how a passphrase prompt and a host-key question reach the person — and
// the tail is kept so that a handshake failure can name the likely cause.
tail := &tailWriter{}
process.Stderr = io.MultiWriter(os.Stderr, tail)
process.Stderr = l.stderrWriter(tail, os.Stderr)
if err := process.Start(); err != nil {
if strings.Contains(err.Error(), "executable file not found") {
return nil, fmt.Errorf("this machine has no ssh on its path, and --host is ssh")
Expand All @@ -211,6 +211,19 @@ func (l *engineLink) spawn() (io.ReadWriteCloser, error) {
return pipePair{r: stdout, w: stdin}, nil
}

// stderrWriter keeps the launch-time prompts visible but keeps redial output
// inside the session's diagnostic tail. A reconnect happens while Bubble Tea
// owns the terminal's alternate screen, so writing ssh's transient errors to
// os.Stderr would paint over the frame instead of becoming a status detail.
func (l *engineLink) stderrWriter(tail *tailWriter, terminal io.Writer) io.Writer {
l.mu.Lock()
defer l.mu.Unlock()
if l.process == nil {
return io.MultiWriter(terminal, tail)
}
return tail
}

// sshTransportArgs keeps the carrier's latency policy in one place. -T remains
// first because a pseudo-terminal changes bytes; the other options keep a warm
// connection available for redials, notice a machine that stopped answering,
Expand Down Expand Up @@ -250,15 +263,22 @@ func sshControlPath() string {
return ""
}
path := filepath.Join(dir, "ctl-%C")
// OpenSSH expands %C to a 40-character SHA-1 digest before bind(2), so the
// expanded path is the one that must fit the shared macOS/Linux ceiling.
expanded := strings.Replace(path, "%C", strings.Repeat("0", 40), 1)
if !enginehost.SocketPathFits(expanded) {
if !sshControlPathFits(path) {
return ""
}
return path
}

// sshControlPathFits accounts for OpenSSH's temporary control-master name as
// well as the final hashed path, so a path accepted here cannot fail at bind.
func sshControlPathFits(path string) bool {
// OpenSSH expands %C to a 40-character SHA-1 digest and briefly appends a
// 17-character suffix before bind(2), so both forms must fit the shared
// macOS/Linux ceiling.
expanded := strings.Replace(path, "%C", strings.Repeat("0", 40), 1)
return enginehost.SocketPathFits(expanded) && enginehost.SocketPathFits(expanded+strings.Repeat("0", 17))
}

// hold takes the new child and lets go of the old one. THE PREVIOUS SSH IS
// REAPED IN THE BACKGROUND, because a redial happens after its pipe died and a
// child nobody waits on is a zombie for as long as this terminal is open — five
Expand Down Expand Up @@ -647,7 +667,7 @@ func hostOptions(fleet *engineFleet, welcome remote.Welcome, pick bool) (tui3.Op
world.prime()
ledger := newHostLedger(far)
ledger.prime()
memory := newHostMemory(far)
memory := newHostMemory(far, welcome.Memory)
memory.prime()

// The counting gate is here and not on the roads: a session this window
Expand Down Expand Up @@ -1557,11 +1577,12 @@ type hostMemory struct {
mu sync.Mutex
shelves store.MemoryShelves
learned, letGo int
enabled bool
known bool
}

func newHostMemory(far hostFar) *hostMemory {
h := &hostMemory{client: far.client}
func newHostMemory(far hostFar, enabled bool) *hostMemory {
h := &hostMemory{client: far.client, enabled: enabled, known: true}
far.arm(&h.duty, "reading what is remembered")
return h
}
Expand Down Expand Up @@ -1618,3 +1639,13 @@ func (h *hostMemory) RestoreMemory(id string) error {
func (h *hostMemory) MemoryProvenance(id string) (string, string, time.Time, error) {
return h.client.MemoryProvenance(id)
}
func (h *hostMemory) Remembers() bool { return h.enabled }
func (h *hostMemory) Remember(text string) (string, error) {
return h.client.Remember(text)
}
func (h *hostMemory) Forget(query string) (string, error) {
return h.client.ForgetQuery(query)
}
func (h *hostMemory) Memories(query string) ([]session.MemoryLine, error) {
return h.client.Memories(query)
}
26 changes: 26 additions & 0 deletions cmd/codeaf/chatv3_host_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"bytes"
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
Expand All @@ -20,6 +22,30 @@ import (
// of a remote session.
var _ tui3.Agent = (*remote.Agent)(nil)

func TestRedialSSHStderrStaysOutOfTheTerminal(t *testing.T) {
link := &engineLink{}
terminal := new(bytes.Buffer)
first := &tailWriter{}
if _, err := link.stderrWriter(first, terminal).Write([]byte("host-key prompt\n")); err != nil {
t.Fatal(err)
}
if terminal.String() != "host-key prompt\n" {
t.Fatalf("the first ssh prompt was not shown: %q", terminal.String())
}

link.process = &exec.Cmd{}
redial := &tailWriter{}
if _, err := link.stderrWriter(redial, terminal).Write([]byte("connection refused\n")); err != nil {
t.Fatal(err)
}
if terminal.String() != "host-key prompt\n" {
t.Fatalf("redial stderr painted the terminal: %q", terminal.String())
}
if got := redial.String(); got != "connection refused\n" {
t.Fatalf("redial stderr was not retained: %q", got)
}
}

func TestParseHostTarget(t *testing.T) {
cases := []struct {
raw string
Expand Down
49 changes: 24 additions & 25 deletions cmd/codeaf/do.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,11 @@ type headlessOutcome struct {
// callers must never parse the bounded person's account, and it is never
// clipped.
Checklist []revision.PointOutcome `json:"checklist,omitempty"`
// Error is the sentence a run that never reached an outcome left behind:
// Error is the sentence a run that could not finish left behind:
// the store that would not open, the working directory that could not be
// made, the resident that never picked the command up, a journal read that
// failed mid-flight. It is empty on every run that produced an answer.
// failed mid-flight, or a worker's reason it could not continue. It is
// empty on every run that produced an answer.
//
// It exists because --json's whole promise is one object on stdout, and a
// promise that only holds when the work succeeds is not one a script can be
Expand Down Expand Up @@ -3403,6 +3404,9 @@ func withoutSummaryFileList(deliverable string, artifacts []string) string {
// most common thing anyone does with a one-shot is pipe it somewhere.
func reportErrand(request doRequest, outcome headlessOutcome) error {
sayBlocked(request.stderr, outcome)
if !request.asJSON && request.stderr != nil && strings.TrimSpace(outcome.Error) != "" {
fmt.Fprintln(request.stderr, "error: "+outcome.Error)
}
if outcome.recordKept != "" && request.stderr != nil {
fmt.Fprintf(request.stderr, "record kept at %s\n", outcome.recordKept)
}
Expand Down Expand Up @@ -3691,6 +3695,9 @@ func runErrand(request doRequest, seats config.Seats) (outcome headlessOutcome,
defer stopSignals()
ctx, cancel := context.WithTimeout(signalled, request.timeout)
defer cancel()
if spendPreauthorized(request.yesSpend, env.Value) {
ctx = session.WithDailySpendPreauthorized(ctx)
}

// THE MACHINE'S HOLD IS SAID, because nothing else here would say it: the
// chat draws `waiting · machine busy` on the run's rail, and a headless run
Expand Down Expand Up @@ -3719,6 +3726,8 @@ func runErrand(request doRequest, seats config.Seats) (outcome headlessOutcome,
Nodes: summary.Nodes,
Seconds: summary.Seconds,
Spend: summary.USD,
tokensIn: summary.TokensIn,
tokensOut: summary.TokensOut,
}
switch summary.Outcome {
case runengine.OutcomeDone:
Expand All @@ -3735,6 +3744,9 @@ func runErrand(request doRequest, seats config.Seats) (outcome headlessOutcome,
default:
// ran and did not finish: a leaf failed, or the clock arrived.
errand.stop, errand.Settled = stopIncomplete, true
// The run already kept the worker's reason; both receipt formats
// need it to explain an incomplete ending.
errand.Error = plainWords(strings.TrimSpace(summary.Failure))
}
// THE RUN'S OWN CLOCK, read off the context because the summary's word is
// the same one a failed leaf leaves and a caller raising a timeout has to be
Expand Down Expand Up @@ -3927,44 +3939,31 @@ func runSpendBound(request doRequest, profileDir string, now time.Time) (runSpen
if err != nil {
return runSpend{}, err
}
daily, err := config.DailyBudgetUSDAt(profileDir)
if err != nil {
return runSpend{}, err
}
bound := runSpend{}
if consent > 0 {
bound = runSpend{usd: consent, stop: stopPrice, words: fmt.Sprintf(
"the run reached $%.2f, the price above which codeaf asks before it spends more; "+
"rerun with --yes-spend to let it go past that", consent)}
}
if daily > 0 {
left := daily - spentToday(now)
if left <= 0 {
return runSpend{refused: true, stop: stopBudget, words: fmt.Sprintf(
"today's spending limit of $%.2f is spent, so nothing was started; "+
"rerun with --yes-spend to spend past it", daily)}, nil
daily, err := session.DailySpendAt(profileDir, now)
if err != nil {
return runSpend{}, err
}
if daily.Limit > 0 {
left := daily.Limit - daily.Spent
if daily.Reached || left <= 0 {
return runSpend{refused: true, stop: stopBudget, words: session.DailySpendAction(daily.Limit) +
"; rerun with --yes-spend to spend past it"}, nil
}
if bound.usd == 0 || left < bound.usd {
bound = runSpend{usd: left, stop: stopBudget, words: fmt.Sprintf(
"the run reached what was left of today's spending limit of $%.2f; "+
"rerun with --yes-spend to spend past it", daily)}
"rerun with --yes-spend to spend past it", daily.Limit)}
}
}
return bound, nil
}

// spentToday is what today has cost on this machine, read off the usage ledger
// every conversation and every run worker writes ([session.SpendToday]). A
// ledger that cannot be read is a day that has spent nothing as far as this
// door can tell; the plan-price rung still bounds the run.
func spentToday(now time.Time) float64 {
lines, err := session.ReadUsage(session.UsageLedgerPath(), now.Add(-48*time.Hour))
if err != nil {
return 0
}
return session.SpendToday(lines, now)
}

// crewCompleters turns the run road's provider seam into the per-model
// completer [runengine.CrewFactory] asks for. The factory reads the crew at every
// launch, so a task's seat model is not known until the task is handed over;
Expand Down
Loading
Loading