Add daemon-attached goal recovery operator
Resume goals after transient API failures with persisted exponential backoff while respecting pauses and interruptions.
This commit is contained in:
parent
a4522e5ed6
commit
e4b84d9802
1 changed files with 1595 additions and 0 deletions
85
codex-keep/main.go
Normal file
85
codex-keep/main.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
socket string
|
||||
state string
|
||||
base time.Duration
|
||||
cap time.Duration
|
||||
poll time.Duration
|
||||
}
|
||||
|
||||
func statePath() string {
|
||||
root := os.Getenv("XDG_STATE_HOME")
|
||||
if root == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
root = filepath.Join(home, ".local", "state")
|
||||
}
|
||||
return filepath.Join(root, "codex-keep", "state.json")
|
||||
}
|
||||
|
||||
func socketPath() string {
|
||||
home := os.Getenv("CODEX_HOME")
|
||||
if home == "" {
|
||||
root, _ := os.UserHomeDir()
|
||||
home = filepath.Join(root, ".codex")
|
||||
}
|
||||
return filepath.Join(home, "app-server-control", "app-server-control.sock")
|
||||
}
|
||||
|
||||
func parseOptions(args []string, output io.Writer) (options, error) {
|
||||
var opts options
|
||||
flags := flag.NewFlagSet("codex-keep", flag.ContinueOnError)
|
||||
flags.SetOutput(output)
|
||||
flags.StringVar(&opts.socket, "socket", socketPath(), "Codex daemon Unix socket")
|
||||
flags.StringVar(&opts.state, "state", statePath(), "Recovery checkpoint")
|
||||
flags.DurationVar(&opts.base, "base-delay", 5*time.Second, "Initial retry delay")
|
||||
flags.DurationVar(&opts.cap, "max-delay", 5*time.Minute, "Retry delay ceiling")
|
||||
flags.DurationVar(&opts.poll, "poll", 5*time.Second, "Reconciliation interval")
|
||||
flags.Usage = func() {
|
||||
_, _ = fmt.Fprintln(output, "Usage: codex-keep [options]")
|
||||
_, _ = fmt.Fprintln(output, "Reconcile failed active goals through the shared Codex daemon.")
|
||||
flags.PrintDefaults()
|
||||
}
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
if len(flags.Args()) != 0 {
|
||||
return opts, errors.New("unexpected positional arguments")
|
||||
}
|
||||
if opts.base <= 0 || opts.cap < opts.base || opts.poll <= 0 {
|
||||
return opts, errors.New("delays must satisfy 0 < base-delay <= max-delay and poll > 0")
|
||||
}
|
||||
if opts.state == "" {
|
||||
return opts, errors.New("state path is empty")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts, err := parseOptions(os.Args[1:], os.Stderr)
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
err = run(ctx, opts, os.Stderr)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "codex-keep:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue