Reconcile goal recovery with human intent and model state transitions
This commit is contained in:
parent
e4b84d9802
commit
548eb69211
1 changed files with 1765 additions and 96 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -15,9 +16,11 @@ type fakeDaemon struct {
|
|||
goal *goal
|
||||
turn turn
|
||||
status string
|
||||
path string
|
||||
requests []message
|
||||
before func(message)
|
||||
lostReply bool
|
||||
setError *rpcError
|
||||
}
|
||||
|
||||
func newFake(t *testing.T, s *store) *fakeDaemon {
|
||||
|
|
@ -30,7 +33,15 @@ func newFake(t *testing.T, s *store) *fakeDaemon {
|
|||
}
|
||||
t.Cleanup(func() { closeStore(t, s) })
|
||||
}
|
||||
f := &fakeDaemon{t: t, goal: testGoal(active), turn: testTurn("inProgress"), status: "systemError"}
|
||||
path := filepath.Join(filepath.Dir(s.path), "rollout.jsonl")
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &fakeDaemon{path: path, t: t, goal: testGoal(active), turn: testTurn("inProgress"), status: "systemError"}
|
||||
c := &connection{frames: make(chan frame, 32)}
|
||||
op := &operator{
|
||||
opts: options{base: time.Second, cap: time.Minute}, store: s, conn: c,
|
||||
|
|
@ -68,21 +79,33 @@ func (f *fakeDaemon) write(data []byte) error {
|
|||
case "thread/goal/get":
|
||||
result = goalResult{f.goal}
|
||||
case "thread/resume":
|
||||
result = struct{}{}
|
||||
result = map[string]any{"thread": map[string]string{"path": f.path}}
|
||||
case "thread/turns/list":
|
||||
result = turnsResult{[]turn{f.turn}}
|
||||
case "thread/read":
|
||||
result = map[string]any{"thread": map[string]any{"status": map[string]string{"type": f.status}}}
|
||||
result = map[string]any{"thread": map[string]any{"path": f.path, "status": map[string]string{"type": f.status}}}
|
||||
case "thread/goal/set":
|
||||
var p map[string]any
|
||||
if err := json.Unmarshal(m.Params, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(p) != 3 || p["threadId"] != "thread-1" || p["status"] != "active" || p["origin"] != "user" {
|
||||
if len(p) != 3 || p["threadId"] != "thread-1" || p["origin"] != "automatic" {
|
||||
f.t.Fatalf("goal changed beyond status: %s", m.Params)
|
||||
}
|
||||
f.goal = testGoal(active)
|
||||
f.turn = turn{ID: "turn-2", Status: "inProgress"}
|
||||
if f.setError != nil {
|
||||
f.enqueue(message{ID: m.ID, Error: f.setError})
|
||||
return nil
|
||||
}
|
||||
if f.goal == nil {
|
||||
f.enqueue(message{ID: m.ID, Error: &rpcError{Code: -32600, Message: "synthetic missing goal"}})
|
||||
return nil
|
||||
}
|
||||
next := *f.goal
|
||||
next.Status = goalStatus(p["status"].(string))
|
||||
f.goal = &next
|
||||
if next.Status == active {
|
||||
f.turn = turn{ID: "turn-2", Status: "inProgress"}
|
||||
}
|
||||
if f.lostReply {
|
||||
f.op.conn.frames <- frame{err: io.EOF}
|
||||
return nil
|
||||
|
|
@ -96,6 +119,9 @@ func (f *fakeDaemon) write(data []byte) error {
|
|||
return err
|
||||
}
|
||||
f.enqueue(message{ID: m.ID, Result: raw})
|
||||
if m.Method == "thread/goal/set" || m.Method == "thread/resume" {
|
||||
f.notify("thread/goal/updated", map[string]any{"threadId": "thread-1", "goal": f.goal})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +174,8 @@ func TestRecovery(t *testing.T) {
|
|||
if f.count("thread/goal/set") != 1 {
|
||||
t.Fatal("resumed active work twice")
|
||||
}
|
||||
if f.count("thread/resume") != 1 {
|
||||
t.Fatal("subscribed repeatedly")
|
||||
if f.count("thread/resume") != 4 {
|
||||
t.Fatal("missing notification barrier")
|
||||
}
|
||||
f.fail()
|
||||
f.turn.ID = "turn-2"
|
||||
|
|
@ -181,7 +207,7 @@ func TestPauseDuringRead(t *testing.T) {
|
|||
w.Due = time.Now().Add(-time.Second)
|
||||
f.op.store.Watches["thread-1"] = w
|
||||
f.before = func(m message) {
|
||||
if m.Method == method {
|
||||
if m.Method == method && w.Source != nil {
|
||||
f.notify("thread/goal/updated", map[string]any{"threadId": "thread-1", "goal": testGoal(paused)})
|
||||
f.before = nil
|
||||
}
|
||||
|
|
@ -273,6 +299,32 @@ func TestLostSetReply(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResetSetError(t *testing.T) {
|
||||
f := newFake(t, nil)
|
||||
f.fail()
|
||||
w := failedWatch()
|
||||
w.Failures = 1
|
||||
w.Due = time.Now().Add(-time.Second)
|
||||
f.op.store.Watches["thread-1"] = w
|
||||
f.setError = &rpcError{Code: -32600, Message: "synthetic rejection"}
|
||||
f.before = func(m message) {
|
||||
if m.Method != "thread/goal/set" {
|
||||
return
|
||||
}
|
||||
f.notify("thread/goal/updated", map[string]any{"threadId": "thread-1", "goal": testGoal(active)})
|
||||
f.notify("turn/started", map[string]any{"threadId": "thread-1", "turn": turn{ID: "turn-2", Status: "inProgress"}})
|
||||
f.goal = testGoal("complete")
|
||||
f.notify("thread/goal/updated", map[string]any{"threadId": "thread-1", "goal": f.goal})
|
||||
f.notify("turn/completed", map[string]any{"threadId": "thread-1", "turn": turn{ID: "turn-2", Status: "completed"}})
|
||||
}
|
||||
if err := f.reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Failures != 0 || w.eligible() || !w.Due.IsZero() {
|
||||
t.Fatalf("stale activation error changed successful work: %+v", w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCompetingRetry(t *testing.T) {
|
||||
f := newFake(t, nil)
|
||||
if err := f.reconcile(); err != nil {
|
||||
|
|
@ -291,3 +343,38 @@ func TestNoCompetingRetry(t *testing.T) {
|
|||
t.Fatal("competed with internal retry")
|
||||
}
|
||||
}
|
||||
|
||||
func intentLine(status goalStatus) []byte {
|
||||
body := "<codex_internal_context source=\"user_goal\">\nUser set goal status: " + string(mustJSON(status)) + ".\n</codex_internal_context>"
|
||||
return append(mustJSON(map[string]any{"type": "response_item", "payload": map[string]any{
|
||||
"type": "message", "role": "user", "content": []any{map[string]string{"type": "input_text", "text": body}},
|
||||
"internal_chat_message_metadata_passthrough": map[string]any{"content_item_kinds": []string{"user.goal"}},
|
||||
}}), '\n')
|
||||
}
|
||||
|
||||
func mustJSON(value any) []byte {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func appendIntent(t *testing.T, path string, data []byte) {
|
||||
t.Helper()
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := f.Write(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeDaemon) human(g *goal) {
|
||||
appendIntent(f.t, f.path, intentLine(g.Status))
|
||||
f.notify("thread/goal/updated", map[string]any{"threadId": "thread-1", "goal": g})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue