Reconcile goal recovery with human intent and model state transitions

This commit is contained in:
ms2k 2026-10-18 19:21:26 +09:00
commit 548eb69211

View file

@ -51,21 +51,25 @@ func (op *operator) event(m message) error {
return nil
}
w := op.store.watch(p.ThreadID)
if m.Method == "turn/completed" && w.Turn.ID != "" && w.Turn.ID != p.Turn.ID {
return nil
}
var inputErr error
if m.Method == "turn/completed" && p.Turn.Status == "interrupted" && w.Source != nil {
inputErr = w.Source.seal()
}
switch m.Method {
case "thread/goal/updated":
w.update(p.Goal)
w.notice(p.Goal)
case "thread/goal/cleared":
w.update(nil)
case "turn/started":
w.observe(p.Turn)
case "turn/completed":
if w.Turn.ID != "" && w.Turn.ID != p.Turn.ID {
return nil
}
w.notice(nil)
default:
w.observe(p.Turn)
}
op.schedule(p.ThreadID)
return op.save()
return errors.Join(inputErr, op.save())
}
func (op *operator) reconcile(ctx context.Context) error {
@ -115,6 +119,33 @@ func (op *operator) reconcile(ctx context.Context) error {
func (op *operator) inspect(ctx context.Context, id string) error {
w := op.store.watch(id)
if w.Source == nil {
var probe goalResult
if err := op.conn.call(ctx, "thread/goal/get", threadParams{id}, &probe); err != nil {
return err
}
if probe.Goal == nil {
w.update(nil)
w.Pending = false
return op.save()
}
var info struct {
Thread struct {
Path string `json:"path"`
} `json:"thread"`
}
if err := op.conn.call(ctx, "thread/read", threadParams{id}, &info); err != nil {
return err
}
source, err := openIntent(info.Thread.Path)
if err != nil {
return err
}
w.Source = source
if !w.Pending {
w.Hold = nil
}
}
revision := w.revision
var g goalResult
if err := op.conn.call(ctx, "thread/goal/get", threadParams{id}, &g); err != nil {
@ -124,27 +155,68 @@ func (op *operator) inspect(ctx context.Context, id string) error {
return nil
}
w.update(g.Goal)
w.Pending = false
if g.Goal == nil {
return op.save()
}
end, err := w.Source.end()
if err != nil {
return err
}
if !op.attached[id] {
p := struct {
ThreadID string `json:"threadId"`
Exclude bool `json:"excludeTurns"`
}{id, true}
if err := op.conn.call(ctx, "thread/resume", p, nil); err != nil {
w.echo = nil
}
p := struct {
ThreadID string `json:"threadId"`
Exclude bool `json:"excludeTurns"`
}{id, true}
var resumed struct {
Thread struct {
Path string `json:"path"`
} `json:"thread"`
}
revision = w.revision
if err := op.conn.call(ctx, "thread/resume", p, &resumed); err != nil {
return err
}
op.attached[id] = true
// The listener orders this reply after earlier notifications and before its goal snapshot.
w.seed = g.Goal
if w.Source.Path != resumed.Thread.Path {
source, err := openIntent(resumed.Thread.Path)
if err != nil {
return err
}
op.attached[id] = true
w.Source, end = source, source.Pos
}
if w.revision != revision {
return op.save()
}
if err := op.inputs(w, end); err != nil {
return err
}
if w.Hold != nil && w.Hold.Status == active && g.Goal.Status == blocked {
w.Hold = nil
w.Goal = g.Goal
w.Known, w.Armed = true, true
}
if w.Hold != nil {
if w.echo != nil {
return op.save()
}
if g.Goal.Status != w.Hold.Status {
return op.setGoal(ctx, id, w.Hold.Status)
}
w.update(g.Goal)
}
revision = w.revision
var turns turnsResult
p := struct {
turnPage := struct {
ThreadID string `json:"threadId"`
Limit int `json:"limit"`
View string `json:"itemsView"`
}{id, 1, "summary"}
if err := op.conn.call(ctx, "thread/turns/list", p, &turns); err != nil {
if err := op.conn.call(ctx, "thread/turns/list", turnPage, &turns); err != nil {
return err
}
if w.revision != revision {
@ -165,6 +237,9 @@ func (op *operator) inspect(ctx context.Context, id string) error {
func (op *operator) resume(ctx context.Context, id string) error {
w := op.store.watch(id)
if w.echo != nil || w.seed != nil {
return nil
}
revision := w.revision
var live struct {
Thread struct {
@ -180,7 +255,8 @@ func (op *operator) resume(ctx context.Context, id string) error {
return nil
}
if live.Thread.Status.Type != "idle" && live.Thread.Status.Type != "systemError" {
w.stop()
w.Due = time.Time{}
w.Turn = turn{}
return op.save()
}
var g goalResult
@ -195,20 +271,73 @@ func (op *operator) resume(ctx context.Context, id string) error {
return op.save()
}
w.Due = time.Now().Add(min(op.opts.cap, op.jitter(backoff(op.opts.base, op.opts.cap, w.Failures+1))))
if err := op.setGoal(ctx, id, active); err != nil {
return err
}
_, _ = fmt.Fprintf(op.log, "codex-keep: %s resumed after API failure\n", id)
return nil
}
func (op *operator) setGoal(ctx context.Context, id string, status goalStatus) error {
w := op.store.watch(id)
revision := w.revision
w.Pending = true
if err := op.save(); err != nil {
return err
}
revision = w.revision
if err := op.conn.call(ctx, "thread/goal/set", setParams{id, active, "user"}, &g); err != nil {
w.Failures++
if saveErr := op.save(); saveErr != nil {
return saveErr
var g goalResult
err := op.conn.call(ctx, "thread/goal/set", setParams{id, status, "automatic"}, &g)
var remote *rpcError
if err == nil || errors.As(err, &remote) {
w.Pending = false
}
if err != nil {
if status == active && w.revision == revision && w.eligible() {
w.Failures++
}
} else {
w.echo = g.Goal
if w.revision == revision {
w.update(g.Goal)
}
}
if saveErr := op.save(); saveErr != nil {
return saveErr
}
return err
}
func (op *operator) inputs(w *watch, end int64) error {
if w.Source == nil {
return nil
}
status, err := w.Source.read(end)
if err != nil {
return err
}
if w.revision == revision {
w.update(g.Goal)
if status == "" {
return nil
}
_, _ = fmt.Fprintf(op.log, "codex-keep: %s resumed after API failure\n", id)
return op.save()
w.revision++
w.Due = time.Time{}
if status == "cleared" {
w.stop()
w.Hold = nil
w.Known = false
return nil
}
if w.Goal == nil {
return nil
}
if status == active && (w.Goal.Status == active || w.Goal.Status == blocked) {
w.Hold = nil
w.Known, w.Armed = true, true
return nil
}
desired := *w.Goal
desired.Status = status
w.Goal, w.Hold = &desired, &desired
w.stop()
w.Known = status == active
return nil
}