Fix goal recovery
This commit is contained in:
parent
3846556035
commit
996f586bc1
29 changed files with 783 additions and 145 deletions
312
codex-keep/failure_test.go
Normal file
312
codex-keep/failure_test.go
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFailureKinds(t *testing.T) {
|
||||||
|
for _, raw := range []string{
|
||||||
|
`{"codexErrorInfo":"cyberPolicy"}`,
|
||||||
|
`{"codexErrorInfo":"bioPolicy"}`,
|
||||||
|
`{"codexErrorInfo":"misalignmentPolicyViolation"}`,
|
||||||
|
`{"codexErrorInfo":"unauthorized"}`,
|
||||||
|
`{"codexErrorInfo":"usageLimitExceeded"}`,
|
||||||
|
`{"codexErrorInfo":"badRequest"}`,
|
||||||
|
`{"codexErrorInfo":"other"}`,
|
||||||
|
`{"codexErrorInfo":{"httpConnectionFailed":{"httpStatusCode":401}}}`,
|
||||||
|
`{"codexErrorInfo":{"futureError":{"httpStatusCode":503}}}`,
|
||||||
|
`{"codexErrorInfo":null}`,
|
||||||
|
`{}`,
|
||||||
|
`null`,
|
||||||
|
} {
|
||||||
|
t.Run(raw, func(t *testing.T) {
|
||||||
|
var failure *turnError
|
||||||
|
if err := json.Unmarshal([]byte(raw), &failure); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
for attempt := 1; attempt <= 2; attempt++ {
|
||||||
|
f.fail()
|
||||||
|
if attempt == 2 {
|
||||||
|
f.turn.ID = "turn-2"
|
||||||
|
}
|
||||||
|
f.turn.Error = failure
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !w.eligible() || w.Failures != attempt || f.count("thread/goal/set") != attempt-1 {
|
||||||
|
t.Fatalf("failed turn did not enter backoff: %+v", w)
|
||||||
|
}
|
||||||
|
if delay := time.Until(w.Due); delay <= 0 || delay > time.Duration(attempt)*time.Second {
|
||||||
|
t.Fatalf("unexpected backoff: %s", delay)
|
||||||
|
}
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != attempt-1 {
|
||||||
|
t.Fatal("resumed before backoff elapsed")
|
||||||
|
}
|
||||||
|
w.Due = time.Now().Add(-time.Second)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != attempt || f.goal.Status != active || f.turn.Status != "inProgress" {
|
||||||
|
t.Fatal("failed turn did not resume work")
|
||||||
|
}
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailedCheckpoint(t *testing.T) {
|
||||||
|
for _, status := range []goalStatus{blocked, "usageLimited"} {
|
||||||
|
t.Run(string(status), func(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
f.goal.Status = status
|
||||||
|
f.turn.Error.Info.Kind = "cyberPolicy"
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
w.Goal, w.Turn, w.Armed, w.Known = f.goal, f.turn, false, true
|
||||||
|
if status == "usageLimited" {
|
||||||
|
w.Hold = f.goal
|
||||||
|
}
|
||||||
|
if err := f.op.save(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := f.op.store.path
|
||||||
|
closeStore(t, f.op.store)
|
||||||
|
s, err := openStore(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer closeStore(t, s)
|
||||||
|
next := newFake(t, s)
|
||||||
|
next.goal, next.turn = f.goal, f.turn
|
||||||
|
if err := next.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := 1
|
||||||
|
if status == "usageLimited" {
|
||||||
|
want = 0
|
||||||
|
}
|
||||||
|
if w := s.watch("thread-1"); !w.eligible() || w.Failures != want || w.Due.IsZero() {
|
||||||
|
t.Fatal("persisted failed turn was not scheduled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageRecovery(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
f.goal.Status = "usageLimited"
|
||||||
|
f.turn.Error.Info.Kind = "usageLimitExceeded"
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
if !w.eligible() || w.Failures != 0 || w.Due.IsZero() {
|
||||||
|
t.Fatal("server usage stop was not scheduled")
|
||||||
|
}
|
||||||
|
allowed := true
|
||||||
|
f.usage = &allowed
|
||||||
|
w.Due = time.Now().Add(-time.Second)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != 1 || f.goal.Status != active {
|
||||||
|
t.Fatal("server usage stop was not resumed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailureStop(t *testing.T) {
|
||||||
|
for _, status := range []string{"interrupted", "paused", "blocked", "usageLimited", "cleared"} {
|
||||||
|
t.Run(status, func(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
f.turn.Error.Info.Kind = "cyberPolicy"
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
if !w.eligible() {
|
||||||
|
t.Fatal("failed turn was not scheduled")
|
||||||
|
}
|
||||||
|
w.Due = time.Now().Add(-time.Second)
|
||||||
|
switch status {
|
||||||
|
case "interrupted":
|
||||||
|
f.turn.Status = status
|
||||||
|
f.notify("turn/completed", params{ThreadID: "thread-1", Turn: f.turn})
|
||||||
|
case "cleared":
|
||||||
|
appendIntent(t, f.path, intentLine("cleared"))
|
||||||
|
f.goal = nil
|
||||||
|
default:
|
||||||
|
f.goal = testGoal(goalStatus(status))
|
||||||
|
f.human(f.goal)
|
||||||
|
}
|
||||||
|
for range 3 {
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != 0 || w.eligible() || !w.Due.IsZero() {
|
||||||
|
t.Fatal("explicit human interruption did not cancel recovery")
|
||||||
|
}
|
||||||
|
path := f.op.store.path
|
||||||
|
closeStore(t, f.op.store)
|
||||||
|
s, err := openStore(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer closeStore(t, s)
|
||||||
|
if w := s.watch("thread-1"); w.eligible() || !w.Due.IsZero() {
|
||||||
|
t.Fatal("restart revoked explicit human cancellation")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockSource(t *testing.T) {
|
||||||
|
for _, source := range []string{"agent", "human"} {
|
||||||
|
for _, status := range []string{"completed", "failed"} {
|
||||||
|
t.Run(source+"/"+status, func(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.goal, f.turn = testGoal(blocked), testTurn(status)
|
||||||
|
if source == "human" {
|
||||||
|
f.human(f.goal)
|
||||||
|
} else {
|
||||||
|
f.agent(f.goal)
|
||||||
|
}
|
||||||
|
f.notify("turn/completed", params{ThreadID: "thread-1", Turn: f.turn})
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
if w.eligible() || f.count("thread/goal/set") != 0 || !w.Due.IsZero() {
|
||||||
|
t.Fatal("explicit block did not cancel recovery")
|
||||||
|
}
|
||||||
|
for range 3 {
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != 0 || f.goal.Status != blocked || !w.Due.IsZero() {
|
||||||
|
t.Fatal("explicitly blocked goal was resumed")
|
||||||
|
}
|
||||||
|
path := f.op.store.path
|
||||||
|
closeStore(t, f.op.store)
|
||||||
|
s, err := openStore(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer closeStore(t, s)
|
||||||
|
next := newFake(t, s)
|
||||||
|
next.goal, next.turn = f.goal, f.turn
|
||||||
|
for range 2 {
|
||||||
|
if err := next.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if next.count("thread/goal/set") != 0 || s.watch("thread-1").eligible() {
|
||||||
|
t.Fatal("restart revoked an explicit block")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentRace(t *testing.T) {
|
||||||
|
for _, method := range []string{"thread/goal/set", "account/rateLimits/read"} {
|
||||||
|
t.Run(method, func(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
if method == "account/rateLimits/read" {
|
||||||
|
f.goal.Status = "usageLimited"
|
||||||
|
ready := true
|
||||||
|
f.usage = &ready
|
||||||
|
}
|
||||||
|
seen := false
|
||||||
|
f.before = func(m message) {
|
||||||
|
if m.Method == method {
|
||||||
|
seen = true
|
||||||
|
f.before = nil
|
||||||
|
f.goal = testGoal(blocked)
|
||||||
|
f.agent(f.goal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
if w.eligible() {
|
||||||
|
w.Due = time.Now().Add(-time.Second)
|
||||||
|
}
|
||||||
|
for range 3 {
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seen || w.eligible() || !w.Due.IsZero() || f.goal.Status != blocked {
|
||||||
|
t.Fatal("in-flight recovery revoked an explicit agent block")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentResume(t *testing.T) {
|
||||||
|
f := newFake(t, nil)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
f.agent(f.goal)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.goal, f.turn = testGoal(active), testTurn("inProgress")
|
||||||
|
f.turn.ID = "turn-2"
|
||||||
|
f.human(f.goal)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.fail()
|
||||||
|
f.turn.ID = "turn-2"
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w := f.op.store.watch("thread-1")
|
||||||
|
if !w.eligible() || w.Due.IsZero() {
|
||||||
|
t.Fatal("explicit resume did not restore server-failure recovery")
|
||||||
|
}
|
||||||
|
w.Due = time.Now().Add(-time.Second)
|
||||||
|
if err := f.reconcile(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f.count("thread/goal/set") != 1 || f.turn.Status != "inProgress" {
|
||||||
|
t.Fatal("server failure did not resume after explicit authorization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type intentLog struct {
|
type intentLog struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Pos int64 `json:"pos"`
|
Pos int64 `json:"pos"`
|
||||||
Fence int64 `json:"fence"`
|
Fence int64 `json:"fence"`
|
||||||
Partial bool `json:"partial,omitempty"`
|
Partial bool `json:"partial,omitempty"`
|
||||||
Sealed bool `json:"sealed,omitempty"`
|
Sealed bool `json:"sealed,omitempty"`
|
||||||
|
Calls map[string]struct{} `json:"calls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func openIntent(path string) (*intentLog, error) {
|
func openIntent(path string) (*intentLog, error) {
|
||||||
|
|
@ -88,7 +89,7 @@ func (l *intentLog) read(end int64) (goalStatus, error) {
|
||||||
l.Pos = next
|
l.Pos = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
status, match, err := parseIntent(line)
|
status, match, err := l.parse(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -99,13 +100,18 @@ func (l *intentLog) read(end int64) (goalStatus, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseIntent(line []byte) (goalStatus, bool, error) {
|
func (l *intentLog) parse(line []byte) (goalStatus, bool, error) {
|
||||||
var record struct {
|
var record struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Payload struct {
|
Payload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content []struct {
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
CallID string `json:"call_id"`
|
||||||
|
Arguments json.RawMessage `json:"arguments"`
|
||||||
|
Output json.RawMessage `json:"output"`
|
||||||
|
Content []struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
|
|
@ -117,13 +123,44 @@ func parseIntent(line []byte) (goalStatus, bool, error) {
|
||||||
if err := json.Unmarshal(line, &record); err != nil {
|
if err := json.Unmarshal(line, &record); err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
if record.Type != "response_item" {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
p := record.Payload
|
p := record.Payload
|
||||||
if record.Type != "response_item" || p.Type != "message" || p.Role != "user" || len(p.Content) != 1 ||
|
switch p.Type {
|
||||||
|
case "function_call":
|
||||||
|
if p.Name == "update_goal" && (p.Namespace == "" || p.Namespace == "functions") && p.CallID != "" {
|
||||||
|
var args struct {
|
||||||
|
Status goalStatus `json:"status"`
|
||||||
|
}
|
||||||
|
var arguments string
|
||||||
|
if json.Unmarshal(p.Arguments, &arguments) == nil && json.Unmarshal([]byte(arguments), &args) == nil && args.Status == blocked {
|
||||||
|
if l.Calls == nil {
|
||||||
|
l.Calls = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
l.Calls[p.CallID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false, nil
|
||||||
|
case "function_call_output":
|
||||||
|
if _, ok := l.Calls[p.CallID]; !ok {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
delete(l.Calls, p.CallID)
|
||||||
|
var output string
|
||||||
|
var result goalResult
|
||||||
|
if json.Unmarshal(p.Output, &output) == nil && json.Unmarshal([]byte(output), &result) == nil &&
|
||||||
|
result.Goal != nil && result.Goal.Status == blocked {
|
||||||
|
return blocked, true, nil
|
||||||
|
}
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
if p.Type != "message" || p.Role != "user" || len(p.Content) != 1 ||
|
||||||
p.Content[0].Type != "input_text" || len(p.Meta.Kinds) != 1 ||
|
p.Content[0].Type != "input_text" || len(p.Meta.Kinds) != 1 ||
|
||||||
p.Meta.Kinds[0] != "user.goal" && p.Meta.Kinds[0] != "user.goal.omitted" {
|
p.Meta.Kinds[0] != "user.goal" && p.Meta.Kinds[0] != "user.goal.omitted" {
|
||||||
return "", false, nil
|
return "", false, nil
|
||||||
}
|
}
|
||||||
for _, line := range strings.Split(p.Content[0].Text, "\n") {
|
for line := range strings.SplitSeq(p.Content[0].Text, "\n") {
|
||||||
if line == "User cleared the goal." {
|
if line == "User cleared the goal." {
|
||||||
return "cleared", true, nil
|
return "cleared", true, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -9,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntentProvenance(t *testing.T) {
|
func TestIntentProvenance(t *testing.T) {
|
||||||
for _, status := range []goalStatus{active, paused, "complete", "budgetLimited", "usageLimited", blocked} {
|
for _, status := range []goalStatus{active, paused, "complete", "budgetLimited", "usageLimited", blocked, "cleared"} {
|
||||||
got, ok, err := parseIntent(intentLine(status))
|
got, ok, err := (&intentLog{}).parse(intentLine(status))
|
||||||
if err != nil || !ok || got != status {
|
if err != nil || !ok || got != status {
|
||||||
t.Fatalf("status=%s got=%s match=%v error=%v", status, got, ok, err)
|
t.Fatalf("status=%s got=%s match=%v error=%v", status, got, ok, err)
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +22,7 @@ func TestIntentProvenance(t *testing.T) {
|
||||||
strings.Replace(line, "\"role\":\"user\"", "\"role\":\"assistant\"", 1),
|
strings.Replace(line, "\"role\":\"user\"", "\"role\":\"assistant\"", 1),
|
||||||
strings.Replace(line, "input_text", "output_text", 1),
|
strings.Replace(line, "input_text", "output_text", 1),
|
||||||
} {
|
} {
|
||||||
if _, ok, err := parseIntent([]byte(text)); err != nil || ok {
|
if _, ok, err := (&intentLog{}).parse([]byte(text)); err != nil || ok {
|
||||||
t.Fatalf("accepted untrusted instruction: match=%v error=%v", ok, err)
|
t.Fatalf("accepted untrusted instruction: match=%v error=%v", ok, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,3 +108,106 @@ func TestIntentCut(t *testing.T) {
|
||||||
t.Fatalf("lost later input: %s %v", status, err)
|
t.Fatalf("lost later input: %s %v", status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func agentCall(id, name, namespace string, status goalStatus) []byte {
|
||||||
|
return append(mustJSON(map[string]any{"type": "response_item", "payload": map[string]any{
|
||||||
|
"type": "function_call", "name": name, "namespace": namespace, "call_id": id,
|
||||||
|
"arguments": string(mustJSON(struct {
|
||||||
|
Status goalStatus `json:"status"`
|
||||||
|
}{status})),
|
||||||
|
}}), '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentReply(id, output string) []byte {
|
||||||
|
return append(mustJSON(map[string]any{"type": "response_item", "payload": map[string]any{
|
||||||
|
"type": "function_call_output", "call_id": id, "output": output,
|
||||||
|
}}), '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentIntent(t *testing.T) {
|
||||||
|
blockedReply := string(mustJSON(goalResult{Goal: testGoal(blocked)}))
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, tool, namespace string
|
||||||
|
status goalStatus
|
||||||
|
id, replyID, output string
|
||||||
|
want goalStatus
|
||||||
|
}{
|
||||||
|
{"plain", "update_goal", "", blocked, "call-1", "call-1", blockedReply, blocked},
|
||||||
|
{"functions", "update_goal", "functions", blocked, "call-1", "call-1", blockedReply, blocked},
|
||||||
|
{"external", "update_goal", "plugin", blocked, "call-1", "call-1", blockedReply, ""},
|
||||||
|
{"read", "get_goal", "", blocked, "call-1", "call-1", blockedReply, ""},
|
||||||
|
{"other tool", "other", "", blocked, "call-1", "call-1", blockedReply, ""},
|
||||||
|
{"other status", "update_goal", "", paused, "call-1", "call-1", blockedReply, ""},
|
||||||
|
{"no id", "update_goal", "", blocked, "", "", blockedReply, ""},
|
||||||
|
{"other id", "update_goal", "", blocked, "call-1", "call-2", blockedReply, ""},
|
||||||
|
{"rejected", "update_goal", "", blocked, "call-1", "call-1", "failed to update goal", ""},
|
||||||
|
{"no goal", "update_goal", "", blocked, "call-1", "call-1", string(mustJSON(goalResult{})), ""},
|
||||||
|
{"not blocked", "update_goal", "", blocked, "call-1", "call-1", string(mustJSON(goalResult{Goal: testGoal(active)})), ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "rollout.jsonl")
|
||||||
|
if err := os.WriteFile(path, nil, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
log, err := openIntent(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
appendIntent(t, path, agentCall(tc.id, tc.tool, tc.namespace, tc.status))
|
||||||
|
if status, err := log.read(math.MaxInt64); err != nil || status != "" {
|
||||||
|
t.Fatalf("unfinished call produced intent %q: %v", status, err)
|
||||||
|
}
|
||||||
|
appendIntent(t, path, agentReply(tc.replyID, tc.output))
|
||||||
|
if status, err := log.read(math.MaxInt64); err != nil || status != tc.want {
|
||||||
|
t.Fatalf("intent=%q want=%q error=%v", status, tc.want, err)
|
||||||
|
}
|
||||||
|
appendIntent(t, path, agentReply(tc.id, blockedReply))
|
||||||
|
if tc.replyID == tc.id {
|
||||||
|
if status, err := log.read(math.MaxInt64); err != nil || status != "" {
|
||||||
|
t.Fatalf("replayed completed call: %q %v", status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentPending(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "rollout.jsonl")
|
||||||
|
if err := os.WriteFile(path, nil, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
log, err := openIntent(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
appendIntent(t, path, agentCall("call-1", "update_goal", "", blocked))
|
||||||
|
if status, err := log.read(math.MaxInt64); err != nil || status != "" {
|
||||||
|
t.Fatalf("unfinished call produced intent %q: %v", status, err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(log)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var restored intentLog
|
||||||
|
if err := json.Unmarshal(data, &restored); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
appendIntent(t, path, agentReply("call-1", string(mustJSON(goalResult{Goal: testGoal(blocked)}))))
|
||||||
|
if status, err := restored.read(math.MaxInt64); err != nil || status != blocked {
|
||||||
|
t.Fatalf("lost pending call across checkpoint: %q %v", status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentOtherItems(t *testing.T) {
|
||||||
|
var log intentLog
|
||||||
|
for _, payload := range []map[string]any{
|
||||||
|
{"type": "tool_search_call", "arguments": map[string]string{"query": "blocked"}},
|
||||||
|
{"type": "function_call", "name": "update_goal", "call_id": "call-1", "arguments": "invalid JSON"},
|
||||||
|
{"type": "function_call_output", "call_id": "call-1", "output": string(mustJSON(goalResult{Goal: testGoal(blocked)}))},
|
||||||
|
} {
|
||||||
|
line := mustJSON(map[string]any{"type": "response_item", "payload": payload})
|
||||||
|
if status, match, err := log.parse(line); err != nil || match || status != "" {
|
||||||
|
t.Fatalf("unrelated item produced intent=%q match=%v error=%v", status, match, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
11
codex-keep/model/BlindUsage.cfg
Normal file
11
codex-keep/model/BlindUsage.cfg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
SPECIFICATION Spec
|
||||||
|
CONSTANTS
|
||||||
|
Mode = "BlindUsage"
|
||||||
|
GoalBound = 1
|
||||||
|
EditBound = 1
|
||||||
|
StampBound = 0
|
||||||
|
TurnBound = 1
|
||||||
|
FailBound = 1
|
||||||
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
|
CHECK_DEADLOCK FALSE
|
||||||
13
codex-keep/model/Controls.cfg
Normal file
13
codex-keep/model/Controls.cfg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
SPECIFICATION PartSpec
|
||||||
|
CONSTANTS
|
||||||
|
First = "paused"
|
||||||
|
Last = "active"
|
||||||
|
Mode = "Reconcile"
|
||||||
|
GoalBound = 1
|
||||||
|
EditBound = 2
|
||||||
|
StampBound = 0
|
||||||
|
TurnBound = 1
|
||||||
|
FailBound = 1
|
||||||
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
|
CHECK_DEADLOCK FALSE
|
||||||
13
codex-keep/model/Controls.tla
Normal file
13
codex-keep/model/Controls.tla
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
----------------------------- MODULE Controls -----------------------------
|
||||||
|
EXTENDS Keep
|
||||||
|
|
||||||
|
\* Bounded append-only controls partition all traces; fair actions never change inputs.
|
||||||
|
CONSTANTS First, Last
|
||||||
|
ASSUME First \in GoalStates /\ Last \in GoalStates
|
||||||
|
Control == <<First, Last>>
|
||||||
|
|
||||||
|
Prefix == Len(inputs') <= Len(Control) /\
|
||||||
|
\A i \in 1..Len(inputs') : inputs'[i] = Control[i]
|
||||||
|
PartNext == Next /\ Prefix
|
||||||
|
PartSpec == Init /\ [][PartNext]_vars /\ Fairness
|
||||||
|
=============================================================================
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK ApiEvidence
|
INVARIANTS TypeOK FailureEvidence
|
||||||
PROPERTIES StopProgress
|
PROPERTIES StopProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
11
codex-keep/model/Filtered.cfg
Normal file
11
codex-keep/model/Filtered.cfg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
SPECIFICATION Spec
|
||||||
|
CONSTANTS
|
||||||
|
Mode = "Filtered"
|
||||||
|
GoalBound = 1
|
||||||
|
EditBound = 1
|
||||||
|
StampBound = 0
|
||||||
|
TurnBound = 1
|
||||||
|
FailBound = 1
|
||||||
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
@ -1,26 +1,33 @@
|
||||||
------------------------------- MODULE Keep -------------------------------
|
------------------------------- MODULE Keep -------------------------------
|
||||||
EXTENDS Naturals, Sequences
|
EXTENDS Naturals, Sequences
|
||||||
|
|
||||||
\* Each connection serializes RPCs; one abandoned write may finish after reconnect. Epochs audit human intent only.
|
\* Each connection serializes RPCs; one abandoned write may finish after reconnect. Epochs audit explicit intent only.
|
||||||
\* Host-authored controls form an append-only input stream; progress assumes eventual connectivity and fair finite work.
|
\* Human controls and successful agent blocks form an append-only input stream; progress assumes eventual connectivity, usage recovery, and fair finite work.
|
||||||
CONSTANTS Mode, GoalBound, EditBound, StampBound, TurnBound, FailBound
|
CONSTANTS Mode, GoalBound, EditBound, StampBound, TurnBound, FailBound
|
||||||
|
|
||||||
GoalStates == {"active", "blocked", "paused", "complete", "cleared",
|
GoalStates == {"active", "blocked", "paused", "complete", "cleared",
|
||||||
"budgetLimited", "usageLimited"}
|
"budgetLimited", "usageLimited"}
|
||||||
TurnStates == {"inProgress", "failed", "completed", "interrupted"}
|
TurnStates == {"inProgress", "failed", "completed", "interrupted"}
|
||||||
Reads == {"pollGoal", "pollTurn", "thread", "check", "attach"}
|
Reads == {"pollGoal", "pollTurn", "thread", "check", "attach", "usage"}
|
||||||
Events == {"goal", "start", "done"}
|
Events == {"goal", "start", "done"}
|
||||||
Episodes == (1..GoalBound) \X (1..TurnBound)
|
Episodes == (1..GoalBound) \X (1..TurnBound)
|
||||||
StopType == [id : 0..GoalBound, status : GoalStates, epoch : 0..EditBound]
|
StopType == [id : 0..GoalBound, status : GoalStates, epoch : 0..EditBound]
|
||||||
|
|
||||||
ASSUME /\ Mode \in {"Baseline", "Local", "Observe", "Unordered", "BarrierOld", "Reconcile"}
|
ASSUME /\ Mode \in {"Baseline", "Local", "Observe", "Unordered", "BarrierOld", "Filtered", "BlindUsage", "Reconcile"}
|
||||||
/\ GoalBound \in Nat \ {0} /\ EditBound \in Nat \ {0}
|
/\ GoalBound \in Nat \ {0} /\ EditBound \in Nat \ {0}
|
||||||
/\ StampBound \in Nat /\ TurnBound \in Nat \ {0}
|
/\ StampBound \in Nat /\ TurnBound \in Nat \ {0}
|
||||||
/\ FailBound \in Nat \ {0}
|
/\ FailBound \in Nat \ {0}
|
||||||
|
|
||||||
Recon == Mode \in {"Observe", "Unordered", "BarrierOld", "Reconcile"}
|
Recon == Mode \in {"Observe", "Unordered", "BarrierOld", "Filtered", "BlindUsage", "Reconcile"}
|
||||||
Durable == Mode \in {"Unordered", "BarrierOld", "Reconcile"}
|
Durable == Mode \in {"Unordered", "BarrierOld", "Filtered", "BlindUsage", "Reconcile"}
|
||||||
Ordered == Mode \in {"BarrierOld", "Reconcile"}
|
Ordered == Mode \in {"BarrierOld", "Filtered", "BlindUsage", "Reconcile"}
|
||||||
|
|
||||||
|
AllFailures == Mode \in {"BlindUsage", "Reconcile"}
|
||||||
|
Waiting == {"blocked"} \cup (IF AllFailures THEN {"usageLimited"} ELSE {})
|
||||||
|
Running == {"active"} \cup Waiting
|
||||||
|
|
||||||
|
\* Error classes are observationally equivalent in the current recovery policy.
|
||||||
|
Failures == IF AllFailures THEN {FALSE} ELSE BOOLEAN
|
||||||
|
|
||||||
GoalType == [id : 0..GoalBound, stamp : 0..StampBound,
|
GoalType == [id : 0..GoalBound, stamp : 0..StampBound,
|
||||||
status : GoalStates]
|
status : GoalStates]
|
||||||
|
|
@ -29,11 +36,11 @@ LiveType == [edits : 0..EditBound, gen : 1..GoalBound, goal : GoalType, turn : T
|
||||||
WatchType == [cut : 0..EditBound, cursor : 0..EditBound, fence : 0..EditBound, goal : GoalType, turn : TurnType, armed : BOOLEAN,
|
WatchType == [cut : 0..EditBound, cursor : 0..EditBound, fence : 0..EditBound, goal : GoalType, turn : TurnType, armed : BOOLEAN,
|
||||||
fails : 0..FailBound, due : {"none", "waiting", "ready"},
|
fails : 0..FailBound, due : {"none", "waiting", "ready"},
|
||||||
hold : GoalStates \cup {"none"}, pending : BOOLEAN, known : BOOLEAN, echo : GoalType, seed : GoalType]
|
hold : GoalStates \cup {"none"}, pending : BOOLEAN, known : BOOLEAN, echo : GoalType, seed : GoalType]
|
||||||
TicketType == [expired : BOOLEAN, status : GoalStates]
|
TicketType == [expired : BOOLEAN, status : GoalStates, quota : BOOLEAN]
|
||||||
MsgType == [kind : Events \cup Reads \cup {"set"}, goal : GoalType,
|
MsgType == [kind : Events \cup Reads \cup {"set"}, goal : GoalType,
|
||||||
turn : TurnType, truth : 0..EditBound, busy : BOOLEAN, ok : BOOLEAN]
|
turn : TurnType, truth : 0..EditBound, busy : BOOLEAN, ok : BOOLEAN]
|
||||||
ActType == [sent : BOOLEAN, view : [goal : 0..GoalBound, turn : 0..(TurnBound + 1)], expected : TicketType,
|
ActType == [sent : BOOLEAN, view : [goal : 0..GoalBound, turn : 0..(TurnBound + 1)], expected : TicketType,
|
||||||
origin : {"user", "automatic"}, evidence : BOOLEAN, recover : BOOLEAN]
|
origin : {"user", "automatic"}, evidence : BOOLEAN, recover : BOOLEAN, usage : BOOLEAN]
|
||||||
|
|
||||||
VARIABLES late, inputs, sample, attached, live, w, wire, online, phase, req, invalid,
|
VARIABLES late, inputs, sample, attached, live, w, wire, online, phase, req, invalid,
|
||||||
ticket, lastAct, known, need, resumed, cancelled, stopWant, stopNeed
|
ticket, lastAct, known, need, resumed, cancelled, stopWant, stopNeed
|
||||||
|
|
@ -49,17 +56,20 @@ Message(kind, state, ok) ==
|
||||||
|
|
||||||
Stop(x) == [x EXCEPT !.armed = FALSE, !.due = "none"]
|
Stop(x) == [x EXCEPT !.armed = FALSE, !.due = "none"]
|
||||||
Increment(x) == [x EXCEPT !.fails = IF @ < FailBound THEN @ + 1 ELSE @]
|
Increment(x) == [x EXCEPT !.fails = IF @ < FailBound THEN @ + 1 ELSE @]
|
||||||
Eligible(x) == x.armed /\ x.goal.id # 0 /\ x.goal.status = "blocked"
|
Retry(t) == t.api \/ AllFailures
|
||||||
/\ x.turn.status = "failed" /\ x.turn.api
|
Eligible(x) == x.armed /\ x.goal.id # 0 /\ x.goal.status \in Waiting
|
||||||
|
/\ x.turn.status = "failed" /\ Retry(x.turn)
|
||||||
Schedule(x) == IF Eligible(x) /\ x.due = "none"
|
Schedule(x) == IF Eligible(x) /\ x.due = "none"
|
||||||
THEN [Increment(x) EXCEPT !.due = "waiting"] ELSE x
|
THEN IF Mode = "Reconcile" /\ x.goal.status = "usageLimited"
|
||||||
|
THEN [x EXCEPT !.due = "ready"]
|
||||||
|
ELSE [Increment(x) EXCEPT !.due = "waiting"] ELSE x
|
||||||
KnownAfter(g) == IF g.id # 0 /\ g.status = "active" THEN known \cup {g.id} ELSE known
|
KnownAfter(g) == IF g.id # 0 /\ g.status = "active" THEN known \cup {g.id} ELSE known
|
||||||
|
|
||||||
BaseGoal(x, g) ==
|
BaseGoal(x, g) ==
|
||||||
LET same == g.id # 0 /\ x.goal.id = g.id
|
LET same == g.id # 0 /\ x.goal.id = g.id
|
||||||
cancel == ~same \/ g.status \notin {"active", "blocked"}
|
cancel == ~same \/ g.status \notin Running
|
||||||
\/ (g.status = "active" /\ x.goal.status # "active")
|
\/ (g.status = "active" /\ x.goal.status # "active")
|
||||||
\/ (g.status = "blocked" /\ x.due # "none"
|
\/ (g.status \in Waiting /\ x.due # "none"
|
||||||
/\ x.goal.stamp # g.stamp)
|
/\ x.goal.stamp # g.stamp)
|
||||||
base == [x EXCEPT !.goal = g, !.fails = IF same THEN @ ELSE 0,
|
base == [x EXCEPT !.goal = g, !.fails = IF same THEN @ ELSE 0,
|
||||||
!.armed = @ /\ ~cancel, !.due = IF cancel THEN "none" ELSE @]
|
!.armed = @ /\ ~cancel, !.due = IF cancel THEN "none" ELSE @]
|
||||||
|
|
@ -71,17 +81,18 @@ GoalUpdate(x, g, fresh) ==
|
||||||
IF ~fresh /\ g.status = "active" /\ x.hold = "none" /\ g.id = x.goal.id THEN x ELSE
|
IF ~fresh /\ g.status = "active" /\ x.hold = "none" /\ g.id = x.goal.id THEN x ELSE
|
||||||
LET same == g.id # 0 /\ g.id = x.goal.id
|
LET same == g.id # 0 /\ g.id = x.goal.id
|
||||||
holding == x.hold # "none" /\ same
|
holding == x.hold # "none" /\ same
|
||||||
stopped == g.id # 0 /\ g.status \notin {"active", "blocked"}
|
stopped == g.id # 0 /\ g.status \notin Running
|
||||||
base == IF ~same THEN [Stop(x) EXCEPT !.fails = 0, !.hold = "none", !.known = FALSE] ELSE x
|
base == IF ~same THEN [Stop(x) EXCEPT !.fails = 0, !.hold = "none", !.known = FALSE] ELSE x
|
||||||
active == g.id # 0 /\ g.status = "active"
|
active == g.id # 0 /\ g.status = "active"
|
||||||
transition == active /\ (~same \/ x.goal.status # "active")
|
transition == (active /\ (~same \/ x.goal.status # "active"))
|
||||||
|
\/ (Mode = "Reconcile" /\ g.status = "usageLimited" /\ (~same \/ x.goal.status # g.status))
|
||||||
updated == [base EXCEPT !.goal = g, !.known = @ \/ active,
|
updated == [base EXCEPT !.goal = g, !.known = @ \/ active,
|
||||||
!.armed = IF active THEN @ \/ transition ELSE IF stopped \/ g.id = 0 THEN FALSE ELSE @,
|
!.armed = IF active THEN @ \/ transition ELSE IF stopped \/ g.id = 0 THEN FALSE ELSE @,
|
||||||
!.due = IF transition \/ stopped \/ g.id = 0 THEN "none" ELSE @,
|
!.due = IF transition \/ stopped \/ g.id = 0 THEN "none" ELSE @,
|
||||||
!.hold = IF (stopped /\ (Durable \/ x.pending))
|
!.hold = IF (stopped /\ (Durable \/ x.pending))
|
||||||
\/ (~same /\ g.status = "blocked" /\ x.pending) THEN g.status ELSE @]
|
\/ (~same /\ g.status \in Waiting /\ x.pending) THEN g.status ELSE @]
|
||||||
IN IF holding
|
IN IF holding
|
||||||
THEN IF ~fresh THEN IF g.status \in {"active", "blocked"} THEN x ELSE [Stop(x) EXCEPT !.goal = g, !.hold = g.status]
|
THEN IF ~fresh THEN IF g.status \in Running THEN x ELSE [Stop(x) EXCEPT !.goal = g, !.hold = g.status]
|
||||||
ELSE IF g.status = x.hold /\ x.echo.id = 0
|
ELSE IF g.status = x.hold /\ x.echo.id = 0
|
||||||
THEN [Stop(x) EXCEPT !.goal = g, !.hold = IF Durable /\ g.status # "active" THEN @ ELSE "none",
|
THEN [Stop(x) EXCEPT !.goal = g, !.hold = IF Durable /\ g.status # "active" THEN @ ELSE "none",
|
||||||
!.armed = g.status = "active",
|
!.armed = g.status = "active",
|
||||||
|
|
@ -96,11 +107,12 @@ TurnUpdate(x, t) ==
|
||||||
started == [base EXCEPT !.armed = (Recon /\ x.armed)
|
started == [base EXCEPT !.armed = (Recon /\ x.armed)
|
||||||
\/ (t.status = "inProgress" /\ x.goal.id # 0 /\ x.goal.status = "active")]
|
\/ (t.status = "inProgress" /\ x.goal.id # 0 /\ x.goal.status = "active")]
|
||||||
terminal == t.status = "interrupted"
|
terminal == t.status = "interrupted"
|
||||||
\/ (t.status = "failed" /\ ~t.api)
|
\/ (t.status = "failed" /\ ~Retry(t))
|
||||||
\/ (t.status = "completed" /\ ~Recon)
|
\/ (t.status = "completed" /\ ~Recon)
|
||||||
settled == IF terminal THEN Stop(base) ELSE base
|
settled == IF terminal THEN Stop(base) ELSE base
|
||||||
result == IF fresh /\ t.status = "inProgress" THEN started ELSE
|
result == IF fresh /\ t.status = "inProgress" THEN started ELSE
|
||||||
IF Recon /\ (fresh \/ x.turn.status = "inProgress") /\ t.status = "failed" /\ t.api
|
IF Recon /\ (fresh \/ x.turn.status = "inProgress")
|
||||||
|
/\ t.status = "failed" /\ Retry(t)
|
||||||
/\ x.known /\ x.hold = "none"
|
/\ x.known /\ x.hold = "none"
|
||||||
THEN [settled EXCEPT !.armed = TRUE] ELSE settled
|
THEN [settled EXCEPT !.armed = TRUE] ELSE settled
|
||||||
held == IF Recon /\ t.status = "interrupted" /\ x.goal.id # 0 /\ x.pending
|
held == IF Recon /\ t.status = "interrupted" /\ x.goal.id # 0 /\ x.pending
|
||||||
|
|
@ -112,7 +124,7 @@ TurnUpdate(x, t) ==
|
||||||
ApplyInput(x, status) ==
|
ApplyInput(x, status) ==
|
||||||
IF status = "cleared" THEN [Stop(x) EXCEPT !.hold = "none", !.known = FALSE] ELSE
|
IF status = "cleared" THEN [Stop(x) EXCEPT !.hold = "none", !.known = FALSE] ELSE
|
||||||
IF x.goal.id = 0 THEN x ELSE
|
IF x.goal.id = 0 THEN x ELSE
|
||||||
IF status = "active" /\ x.goal.status \in {"active", "blocked"}
|
IF status = "active" /\ x.goal.status \in Running
|
||||||
THEN [x EXCEPT !.hold = "none", !.known = TRUE, !.armed = TRUE, !.due = "none"]
|
THEN [x EXCEPT !.hold = "none", !.known = TRUE, !.armed = TRUE, !.due = "none"]
|
||||||
ELSE [Stop(x) EXCEPT !.goal.status = status, !.hold = status, !.known = status = "active"]
|
ELSE [Stop(x) EXCEPT !.goal.status = status, !.hold = status, !.known = status = "active"]
|
||||||
|
|
||||||
|
|
@ -136,9 +148,9 @@ Init ==
|
||||||
armed |-> TRUE, fails |-> 0, due |-> "none", hold |-> "none", pending |-> FALSE, known |-> TRUE, echo |-> EmptyGoal, seed |-> EmptyGoal]
|
armed |-> TRUE, fails |-> 0, due |-> "none", hold |-> "none", pending |-> FALSE, known |-> TRUE, echo |-> EmptyGoal, seed |-> EmptyGoal]
|
||||||
/\ wire = <<>> /\ online = TRUE /\ phase = "idle" /\ req = "none"
|
/\ wire = <<>> /\ online = TRUE /\ phase = "idle" /\ req = "none"
|
||||||
/\ invalid = FALSE
|
/\ invalid = FALSE
|
||||||
/\ ticket = [expired |-> FALSE, status |-> "active"]
|
/\ ticket = [expired |-> FALSE, status |-> "active", quota |-> FALSE]
|
||||||
/\ lastAct = [sent |-> FALSE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id], expected |-> ticket,
|
/\ lastAct = [sent |-> FALSE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id], expected |-> ticket,
|
||||||
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE]
|
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE, usage |-> FALSE]
|
||||||
/\ known = {1} /\ need = {} /\ resumed = {} /\ cancelled = {}
|
/\ known = {1} /\ need = {} /\ resumed = {} /\ cancelled = {}
|
||||||
/\ stopWant = [id |-> 0, status |-> "paused", epoch |-> 0] /\ stopNeed = FALSE
|
/\ stopWant = [id |-> 0, status |-> "paused", epoch |-> 0] /\ stopNeed = FALSE
|
||||||
|
|
||||||
|
|
@ -193,10 +205,10 @@ FinishTurn(status) ==
|
||||||
/\ UNCHANGED <<late, inputs, sample, attached, w, online, phase, req, invalid,
|
/\ UNCHANGED <<late, inputs, sample, attached, w, online, phase, req, invalid,
|
||||||
ticket, lastAct, known, need, resumed, stopWant, stopNeed>>
|
ticket, lastAct, known, need, resumed, stopWant, stopNeed>>
|
||||||
|
|
||||||
FailTurn(isApi, stamp) ==
|
FailTurn(isApi, status, stamp) ==
|
||||||
/\ live.turn.status = "inProgress" /\ live.turn.id <= TurnBound
|
/\ live.turn.status = "inProgress" /\ live.turn.id <= TurnBound
|
||||||
/\ LET g == IF live.goal.status = "active"
|
/\ LET g == IF live.goal.status = "active"
|
||||||
THEN [live.goal EXCEPT !.status = "blocked",
|
THEN [live.goal EXCEPT !.status = status,
|
||||||
!.stamp = stamp]
|
!.stamp = stamp]
|
||||||
ELSE live.goal
|
ELSE live.goal
|
||||||
state == [live EXCEPT !.goal = g, !.turn.status = "failed", !.turn.api = isApi]
|
state == [live EXCEPT !.goal = g, !.turn.status = "failed", !.turn.api = isApi]
|
||||||
|
|
@ -204,7 +216,7 @@ FailTurn(isApi, stamp) ==
|
||||||
THEN Append(wire, Message("goal", state, TRUE)) ELSE wire
|
THEN Append(wire, Message("goal", state, TRUE)) ELSE wire
|
||||||
IN /\ live' = state
|
IN /\ live' = state
|
||||||
/\ wire' = IF online THEN Append(events, Message("done", state, TRUE)) ELSE wire
|
/\ wire' = IF online THEN Append(events, Message("done", state, TRUE)) ELSE wire
|
||||||
/\ need' = IF isApi /\ live.goal.status = "active" /\ live.goal.id \in known
|
/\ need' = IF live.goal.status = "active" /\ live.goal.id \in known
|
||||||
THEN need \cup {<<live.goal.id, live.turn.id>>} ELSE need
|
THEN need \cup {<<live.goal.id, live.turn.id>>} ELSE need
|
||||||
/\ UNCHANGED <<late, inputs, sample, attached, w, online, phase, req, invalid,
|
/\ UNCHANGED <<late, inputs, sample, attached, w, online, phase, req, invalid,
|
||||||
ticket, lastAct, known, resumed, cancelled, stopWant, stopNeed>>
|
ticket, lastAct, known, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
@ -224,7 +236,7 @@ Poll ==
|
||||||
known, need, resumed, cancelled, stopWant, stopNeed>>
|
known, need, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
||||||
ServeRead ==
|
ServeRead ==
|
||||||
/\ req \in Reads
|
/\ req \in Reads \ {"usage"}
|
||||||
/\ wire' = IF ~online THEN wire ELSE
|
/\ wire' = IF ~online THEN wire ELSE
|
||||||
IF req = "attach" THEN Append(Append(wire, Message(req, live, TRUE)), Message("goal", live, TRUE))
|
IF req = "attach" THEN Append(Append(wire, Message(req, live, TRUE)), Message("goal", live, TRUE))
|
||||||
ELSE Append(wire, Message(req, live, TRUE))
|
ELSE Append(wire, Message(req, live, TRUE))
|
||||||
|
|
@ -232,6 +244,14 @@ ServeRead ==
|
||||||
/\ UNCHANGED <<late, inputs, sample, attached, live, w, online, phase, invalid, ticket, lastAct,
|
/\ UNCHANGED <<late, inputs, sample, attached, live, w, online, phase, invalid, ticket, lastAct,
|
||||||
known, need, resumed, cancelled, stopWant, stopNeed>>
|
known, need, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
||||||
|
ServeUsage(allowed) ==
|
||||||
|
/\ req = "usage"
|
||||||
|
/\ wire' = IF ~online THEN wire ELSE
|
||||||
|
Append(wire, Message(req, live, allowed))
|
||||||
|
/\ req' = "none"
|
||||||
|
/\ UNCHANGED <<late, inputs, sample, attached, live, w, online, phase, invalid, ticket, lastAct,
|
||||||
|
known, need, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
||||||
ReadWatch(m) ==
|
ReadWatch(m) ==
|
||||||
IF invalid /\ ~(Mode = "BarrierOld" /\ m.kind = "attach")
|
IF invalid /\ ~(Mode = "BarrierOld" /\ m.kind = "attach")
|
||||||
THEN IF Ordered /\ m.kind = "attach"
|
THEN IF Ordered /\ m.kind = "attach"
|
||||||
|
|
@ -246,6 +266,7 @@ ReadWatch(m) ==
|
||||||
IN IF Recon /\ ~attached /\ m.goal.id # 0
|
IN IF Recon /\ ~attached /\ m.goal.id # 0
|
||||||
THEN [ready EXCEPT !.seed = IF Ordered THEN EmptyGoal ELSE m.goal, !.echo = EmptyGoal] ELSE ready
|
THEN [ready EXCEPT !.seed = IF Ordered THEN EmptyGoal ELSE m.goal, !.echo = EmptyGoal] ELSE ready
|
||||||
[] m.kind = "attach" -> IF Ordered THEN [w EXCEPT !.seed = [id |-> 1, stamp |-> 0, status |-> "active"]] ELSE w
|
[] m.kind = "attach" -> IF Ordered THEN [w EXCEPT !.seed = [id |-> 1, stamp |-> 0, status |-> "active"]] ELSE w
|
||||||
|
[] m.kind = "usage" -> w
|
||||||
[] m.kind = "pollTurn" -> Schedule(TurnUpdate(w, m.turn))
|
[] m.kind = "pollTurn" -> Schedule(TurnUpdate(w, m.turn))
|
||||||
[] m.kind = "thread" -> IF ~m.busy THEN w ELSE
|
[] m.kind = "thread" -> IF ~m.busy THEN w ELSE
|
||||||
IF Recon THEN [w EXCEPT !.due = "none",
|
IF Recon THEN [w EXCEPT !.due = "none",
|
||||||
|
|
@ -261,7 +282,9 @@ ReadPhase(m, updated) ==
|
||||||
[] m.kind = "pollTurn" -> IF Eligible(updated) /\ updated.due = "ready" /\ updated.echo.id = 0 /\ updated.seed.id = 0
|
[] m.kind = "pollTurn" -> IF Eligible(updated) /\ updated.due = "ready" /\ updated.echo.id = 0 /\ updated.seed.id = 0
|
||||||
THEN "thread" ELSE "idle"
|
THEN "thread" ELSE "idle"
|
||||||
[] m.kind = "thread" -> IF m.busy THEN "idle" ELSE "check"
|
[] m.kind = "thread" -> IF m.busy THEN "idle" ELSE "check"
|
||||||
[] OTHER -> IF Eligible(updated) THEN "send" ELSE "idle"
|
[] m.kind = "usage" -> IF m.ok /\ Eligible(updated) THEN "send" ELSE "idle"
|
||||||
|
[] OTHER -> IF ~Eligible(updated) THEN "idle" ELSE
|
||||||
|
IF Mode = "Reconcile" /\ updated.goal.status = "usageLimited" THEN "usage" ELSE "send"
|
||||||
|
|
||||||
ReceiveRead ==
|
ReceiveRead ==
|
||||||
/\ online /\ Len(wire) > 0 /\ Head(wire).kind \in Reads
|
/\ online /\ Len(wire) > 0 /\ Head(wire).kind \in Reads
|
||||||
|
|
@ -272,9 +295,10 @@ ReceiveRead ==
|
||||||
IN /\ w' = [updated EXCEPT !.cut = IF Ordered /\ m.kind = "pollGoal" /\ next = "attach" THEN Len(inputs) ELSE @]
|
IN /\ w' = [updated EXCEPT !.cut = IF Ordered /\ m.kind = "pollGoal" /\ next = "attach" THEN Len(inputs) ELSE @]
|
||||||
/\ phase' = next
|
/\ phase' = next
|
||||||
/\ req' = IF next \in Reads THEN next ELSE "none"
|
/\ req' = IF next \in Reads THEN next ELSE "none"
|
||||||
/\ ticket' = IF next = "send"
|
/\ ticket' = IF m.kind = "usage" THEN [ticket EXCEPT !.quota = m.ok]
|
||||||
THEN [expired |-> w.due = "ready", status |-> "active"]
|
ELSE IF next \in {"send", "usage"}
|
||||||
ELSE ticket
|
THEN [expired |-> w.due = "ready", status |-> "active", quota |-> FALSE]
|
||||||
|
ELSE ticket
|
||||||
/\ known' = IF ~invalid /\ m.kind \in {"pollGoal", "check"}
|
/\ known' = IF ~invalid /\ m.kind \in {"pollGoal", "check"}
|
||||||
THEN KnownAfter(m.goal) ELSE known
|
THEN KnownAfter(m.goal) ELSE known
|
||||||
/\ sample' = IF Head(wire).kind = "pollGoal" /\ ~invalid THEN Head(wire).goal ELSE sample
|
/\ sample' = IF Head(wire).kind = "pollGoal" /\ ~invalid THEN Head(wire).goal ELSE sample
|
||||||
|
|
@ -283,7 +307,7 @@ ReceiveRead ==
|
||||||
/\ attached' = (attached \/ Head(wire).kind = "attach")
|
/\ attached' = (attached \/ Head(wire).kind = "attach")
|
||||||
/\ LET m == Head(wire)
|
/\ LET m == Head(wire)
|
||||||
stopped == ~invalid /\ m.kind = "pollGoal" /\ m.goal.id # 0
|
stopped == ~invalid /\ m.kind = "pollGoal" /\ m.goal.id # 0
|
||||||
/\ m.goal.status \notin {"active", "blocked"}
|
/\ m.goal.status \notin Running
|
||||||
/\ (ReadWatch(m)).goal.status = m.goal.status
|
/\ (ReadWatch(m)).goal.status = m.goal.status
|
||||||
IN /\ stopWant' = IF stopped THEN [id |-> m.goal.id, status |-> m.goal.status, epoch |-> m.truth] ELSE stopWant
|
IN /\ stopWant' = IF stopped THEN [id |-> m.goal.id, status |-> m.goal.status, epoch |-> m.truth] ELSE stopWant
|
||||||
/\ stopNeed' = (stopNeed \/ stopped)
|
/\ stopNeed' = (stopNeed \/ stopped)
|
||||||
|
|
@ -293,7 +317,7 @@ ReceiveRead ==
|
||||||
ReadInput ==
|
ReadInput ==
|
||||||
/\ online /\ phase = "inputs" /\ req = "none"
|
/\ online /\ phase = "inputs" /\ req = "none"
|
||||||
/\ LET updated == SyncInput(w)
|
/\ LET updated == SyncInput(w)
|
||||||
blocked == updated.hold = "active" /\ sample.status = "blocked"
|
blocked == updated.hold = "active" /\ sample.status \in Waiting
|
||||||
ready == IF blocked THEN [updated EXCEPT !.hold = "none", !.goal = sample,
|
ready == IF blocked THEN [updated EXCEPT !.hold = "none", !.goal = sample,
|
||||||
!.known = TRUE, !.armed = TRUE] ELSE updated
|
!.known = TRUE, !.armed = TRUE] ELSE updated
|
||||||
repair == ready.hold # "none" /\ ready.hold # sample.status
|
repair == ready.hold # "none" /\ ready.hold # sample.status
|
||||||
|
|
@ -316,7 +340,7 @@ ReceiveEvent ==
|
||||||
echo == Recon /\ m.kind = "goal" /\ w.echo.id # 0
|
echo == Recon /\ m.kind = "goal" /\ w.echo.id # 0
|
||||||
/\ m.goal.id = w.echo.id /\ m.goal.status = w.echo.status /\ m.goal.stamp = w.echo.stamp
|
/\ m.goal.id = w.echo.id /\ m.goal.status = w.echo.status /\ m.goal.stamp = w.echo.stamp
|
||||||
ignore == snapshot \/ echo \/ (m.kind = "done" /\ m.turn.id # w.turn.id)
|
ignore == snapshot \/ echo \/ (m.kind = "done" /\ m.turn.id # w.turn.id)
|
||||||
seal == ~ignore /\ ((~Ordered /\ m.kind = "goal" /\ m.goal.status \notin {"active", "blocked"})
|
seal == ~ignore /\ ((~Ordered /\ m.kind = "goal" /\ m.goal.status \notin Running)
|
||||||
\/ (m.kind = "done" /\ m.turn.status = "interrupted"))
|
\/ (m.kind = "done" /\ m.turn.status = "interrupted"))
|
||||||
base == IF seal /\ Recon THEN [w EXCEPT !.fence = Len(inputs)]
|
base == IF seal /\ Recon THEN [w EXCEPT !.fence = Len(inputs)]
|
||||||
ELSE IF Ordered \/ (ignore /\ ~echo /\ ~snapshot) THEN w ELSE SyncInput(w)
|
ELSE IF Ordered \/ (ignore /\ ~echo /\ ~snapshot) THEN w ELSE SyncInput(w)
|
||||||
|
|
@ -327,12 +351,12 @@ ReceiveEvent ==
|
||||||
ELSE IF ignore THEN w ELSE Schedule(updated)
|
ELSE IF ignore THEN w ELSE Schedule(updated)
|
||||||
EXCEPT !.seed = IF seed THEN EmptyGoal ELSE @]
|
EXCEPT !.seed = IF seed THEN EmptyGoal ELSE @]
|
||||||
/\ known' = IF m.kind = "goal" THEN KnownAfter(m.goal) ELSE known
|
/\ known' = IF m.kind = "goal" THEN KnownAfter(m.goal) ELSE known
|
||||||
/\ stopWant' = IF ~echo /\ ~snapshot /\ m.kind = "goal" /\ m.goal.id # 0 /\ m.goal.status \notin {"active", "blocked"}
|
/\ stopWant' = IF ~echo /\ ~snapshot /\ m.kind = "goal" /\ m.goal.id # 0 /\ m.goal.status \notin Running
|
||||||
THEN [id |-> m.goal.id, status |-> m.goal.status, epoch |-> m.truth]
|
THEN [id |-> m.goal.id, status |-> m.goal.status, epoch |-> m.truth]
|
||||||
ELSE IF ~ignore /\ m.kind = "done" /\ m.turn.status = "interrupted" /\ w.goal.id # 0 /\ w.pending
|
ELSE IF ~ignore /\ m.kind = "done" /\ m.turn.status = "interrupted" /\ w.goal.id # 0 /\ w.pending
|
||||||
THEN [id |-> w.goal.id, status |-> "paused", epoch |-> m.truth] ELSE stopWant
|
THEN [id |-> w.goal.id, status |-> "paused", epoch |-> m.truth] ELSE stopWant
|
||||||
/\ stopNeed' = ((stopNeed /\ ~(~seal /\ NewInput(w)))
|
/\ stopNeed' = ((stopNeed /\ ~(~seal /\ NewInput(w)))
|
||||||
\/ (~echo /\ ~snapshot /\ m.kind = "goal" /\ m.goal.id # 0 /\ m.goal.status \notin {"active", "blocked"})
|
\/ (~echo /\ ~snapshot /\ m.kind = "goal" /\ m.goal.id # 0 /\ m.goal.status \notin Running)
|
||||||
\/ (~ignore /\ m.kind = "done" /\ m.turn.status = "interrupted" /\ w.goal.id # 0 /\ w.pending))
|
\/ (~ignore /\ m.kind = "done" /\ m.turn.status = "interrupted" /\ w.goal.id # 0 /\ w.pending))
|
||||||
/\ invalid' = (invalid \/ ((~ignore \/ (~Ordered /\ (echo \/ snapshot) /\ NewInput(w))) /\ phase # "idle"))
|
/\ invalid' = (invalid \/ ((~ignore \/ (~Ordered /\ (echo \/ snapshot) /\ NewInput(w))) /\ phase # "idle"))
|
||||||
/\ wire' = Tail(wire)
|
/\ wire' = Tail(wire)
|
||||||
|
|
@ -344,11 +368,11 @@ SendSet ==
|
||||||
/\ IF phase = "send" THEN Eligible(w) ELSE w.hold # "none"
|
/\ IF phase = "send" THEN Eligible(w) ELSE w.hold # "none"
|
||||||
/\ phase' = "set" /\ req' = "set" /\ invalid' = FALSE /\ w' = [w EXCEPT !.pending = TRUE]
|
/\ phase' = "set" /\ req' = "set" /\ invalid' = FALSE /\ w' = [w EXCEPT !.pending = TRUE]
|
||||||
/\ ticket' = IF phase = "repair"
|
/\ ticket' = IF phase = "repair"
|
||||||
THEN [expired |-> FALSE, status |-> w.hold] ELSE ticket
|
THEN [expired |-> FALSE, status |-> w.hold, quota |-> FALSE] ELSE ticket
|
||||||
/\ lastAct' = [sent |-> TRUE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id],
|
/\ lastAct' = [sent |-> TRUE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id],
|
||||||
expected |-> IF phase = "send" THEN ticket ELSE [ticket EXCEPT !.status = w.hold],
|
expected |-> IF phase = "send" THEN ticket ELSE [ticket EXCEPT !.status = w.hold],
|
||||||
origin |-> IF Mode = "Baseline" THEN "user" ELSE "automatic",
|
origin |-> IF Mode = "Baseline" THEN "user" ELSE "automatic",
|
||||||
evidence |-> Eligible(w), recover |-> phase = "send"]
|
evidence |-> Eligible(w), recover |-> phase = "send", usage |-> w.goal.status = "usageLimited"]
|
||||||
/\ UNCHANGED <<late, inputs, sample, attached, live, wire, online, known, need, resumed, cancelled, stopWant, stopNeed>>
|
/\ UNCHANGED <<late, inputs, sample, attached, live, wire, online, known, need, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
||||||
ServeSet(stamp) ==
|
ServeSet(stamp) ==
|
||||||
|
|
@ -371,7 +395,8 @@ ReceiveSet ==
|
||||||
base == [w EXCEPT !.pending = FALSE, !.echo = IF m.ok /\ Recon THEN m.goal ELSE @]
|
base == [w EXCEPT !.pending = FALSE, !.echo = IF m.ok /\ Recon THEN m.goal ELSE @]
|
||||||
IN /\ w' = IF m.ok
|
IN /\ w' = IF m.ok
|
||||||
THEN IF invalid THEN base ELSE GoalUpdate(base, m.goal, TRUE)
|
THEN IF invalid THEN base ELSE GoalUpdate(base, m.goal, TRUE)
|
||||||
ELSE IF Mode = "Baseline" \/ (~invalid /\ Eligible(w))
|
ELSE IF Mode = "Baseline" \/ (~invalid /\ Eligible(w)
|
||||||
|
/\ (Mode # "Reconcile" \/ w.goal.status # "usageLimited"))
|
||||||
THEN Increment(base) ELSE base
|
THEN Increment(base) ELSE base
|
||||||
/\ known' = IF m.ok /\ ~invalid THEN KnownAfter(m.goal) ELSE known
|
/\ known' = IF m.ok /\ ~invalid THEN KnownAfter(m.goal) ELSE known
|
||||||
/\ phase' = "idle" /\ invalid' = FALSE /\ wire' = Tail(wire)
|
/\ phase' = "idle" /\ invalid' = FALSE /\ wire' = Tail(wire)
|
||||||
|
|
@ -390,7 +415,8 @@ Disconnect ==
|
||||||
/\ req' = "none"
|
/\ req' = "none"
|
||||||
/\ late' = IF req = "set" THEN ticket.status ELSE late
|
/\ late' = IF req = "set" THEN ticket.status ELSE late
|
||||||
/\ invalid' = FALSE
|
/\ invalid' = FALSE
|
||||||
/\ w' = [IF phase = "set" /\ (Mode = "Baseline" \/ (~invalid /\ Eligible(w)))
|
/\ w' = [IF phase = "set" /\ (Mode = "Baseline" \/ (~invalid /\ Eligible(w)
|
||||||
|
/\ (Mode # "Reconcile" \/ w.goal.status # "usageLimited")))
|
||||||
THEN Increment(w) ELSE w EXCEPT !.echo = EmptyGoal, !.seed = EmptyGoal]
|
THEN Increment(w) ELSE w EXCEPT !.echo = EmptyGoal, !.seed = EmptyGoal]
|
||||||
/\ UNCHANGED <<inputs, sample, live, ticket, lastAct, known, need, resumed, cancelled, stopWant, stopNeed>>
|
/\ UNCHANGED <<inputs, sample, live, ticket, lastAct, known, need, resumed, cancelled, stopWant, stopNeed>>
|
||||||
|
|
||||||
|
|
@ -421,8 +447,8 @@ Next ==
|
||||||
\/ \E stamp \in live.goal.stamp..StampBound : ReplaceGoal(stamp)
|
\/ \E stamp \in live.goal.stamp..StampBound : ReplaceGoal(stamp)
|
||||||
\/ StartTurn
|
\/ StartTurn
|
||||||
\/ \E status \in {"completed", "interrupted"} : FinishTurn(status)
|
\/ \E status \in {"completed", "interrupted"} : FinishTurn(status)
|
||||||
\/ \E isApi \in BOOLEAN, stamp \in live.goal.stamp..StampBound : FailTurn(isApi, stamp)
|
\/ \E isApi \in Failures, status \in Waiting, stamp \in live.goal.stamp..StampBound : FailTurn(isApi, status, stamp)
|
||||||
\/ Poll \/ ServeRead \/ Receive \/ SendSet \/ ReadInput
|
\/ Poll \/ ServeRead \/ ServeUsage(TRUE) \/ ServeUsage(FALSE) \/ Receive \/ SendSet \/ ReadInput
|
||||||
\/ \E stamp \in live.goal.stamp..StampBound : ServeSet(stamp)
|
\/ \E stamp \in live.goal.stamp..StampBound : ServeSet(stamp)
|
||||||
\/ (\E stamp \in live.goal.stamp..StampBound : ServeLate(stamp))
|
\/ (\E stamp \in live.goal.stamp..StampBound : ServeLate(stamp))
|
||||||
\/ Clock \/ Disconnect \/ Reconnect \/ HonourStop
|
\/ Clock \/ Disconnect \/ Reconnect \/ HonourStop
|
||||||
|
|
@ -442,12 +468,13 @@ TypeOK ==
|
||||||
/\ stopWant \in StopType /\ stopNeed \in BOOLEAN
|
/\ stopWant \in StopType /\ stopNeed \in BOOLEAN
|
||||||
|
|
||||||
FreshRepair == phase # "repair" \/ ~invalid
|
FreshRepair == phase # "repair" \/ ~invalid
|
||||||
ApiEvidence == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.evidence
|
FailureEvidence == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.evidence
|
||||||
AutoProvenance == ~lastAct.sent \/ lastAct.origin = "automatic"
|
AutoProvenance == ~lastAct.sent \/ lastAct.origin = "automatic"
|
||||||
SuccessReset == w.turn.status # "completed" \/ w.fails = 0
|
SuccessReset == w.turn.status # "completed" \/ w.fails = 0
|
||||||
|
UsageEvidence == ~lastAct.sent \/ ~lastAct.recover \/ ~lastAct.usage \/ lastAct.expected.quota
|
||||||
NoEarlyResume == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.expected.expired
|
NoEarlyResume == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.expected.expired
|
||||||
StopCancels == w.hold = "none" \/ (~w.armed /\ w.due = "none")
|
StopCancels == w.hold = "none" \/ (~w.armed /\ w.due = "none")
|
||||||
HumanStops == (w.goal.status \notin {"active", "blocked"} \/ w.turn.status = "interrupted")
|
HumanStops == (w.goal.status \notin Running \/ w.turn.status = "interrupted")
|
||||||
=> (~Eligible(w) /\ w.due = "none")
|
=> (~Eligible(w) /\ w.due = "none")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -466,5 +493,6 @@ FairSet == \E stamp \in live.goal.stamp..StampBound : ServeSet(stamp)
|
||||||
Fairness == WF_vars(Poll) /\ WF_vars(ServeRead) /\ WF_vars(Receive)
|
Fairness == WF_vars(Poll) /\ WF_vars(ServeRead) /\ WF_vars(Receive)
|
||||||
/\ WF_vars(SendSet) /\ WF_vars(FairSet) /\ WF_vars(Clock) /\ WF_vars(StartTurn)
|
/\ WF_vars(SendSet) /\ WF_vars(FairSet) /\ WF_vars(Clock) /\ WF_vars(StartTurn)
|
||||||
/\ WF_vars(FairLate) /\ WF_vars(HonourStop) /\ WF_vars(ReadInput)
|
/\ WF_vars(FairLate) /\ WF_vars(HonourStop) /\ WF_vars(ReadInput)
|
||||||
|
/\ SF_vars(ServeUsage(TRUE))
|
||||||
Spec == Init /\ [][Next]_vars /\ Fairness
|
Spec == Init /\ [][Next]_vars /\ Fairness
|
||||||
=============================================================================
|
=============================================================================
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 2
|
TurnBound = 2
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK AutoProvenance SuccessReset ApiEvidence NoEarlyResume
|
INVARIANTS TypeOK AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence
|
||||||
PROPERTIES RecoveryProgress
|
PROPERTIES RecoveryProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 1
|
StampBound = 1
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK AutoProvenance SuccessReset ApiEvidence NoEarlyResume
|
INVARIANTS TypeOK AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence
|
||||||
PROPERTIES RecoveryProgress
|
PROPERTIES RecoveryProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK AutoProvenance SuccessReset ApiEvidence NoEarlyResume
|
INVARIANTS TypeOK AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence
|
||||||
PROPERTIES StopProgress
|
PROPERTIES StopProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 2
|
TurnBound = 2
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK ApiEvidence
|
INVARIANTS TypeOK FailureEvidence
|
||||||
PROPERTIES RecoveryProgress
|
PROPERTIES RecoveryProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES QueueProgress
|
PROPERTIES QueueProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ QueueInit ==
|
||||||
armed |-> TRUE, fails |-> 0, due |-> "none", hold |-> "none", pending |-> FALSE, known |-> TRUE, echo |-> EmptyGoal, seed |-> EmptyGoal]
|
armed |-> TRUE, fails |-> 0, due |-> "none", hold |-> "none", pending |-> FALSE, known |-> TRUE, echo |-> EmptyGoal, seed |-> EmptyGoal]
|
||||||
/\ wire = <<Message("goal", [live EXCEPT !.goal.status = "paused", !.edits = 1], TRUE), Message("goal", live, TRUE)>> /\ online = TRUE /\ phase = "idle" /\ req = "none"
|
/\ wire = <<Message("goal", [live EXCEPT !.goal.status = "paused", !.edits = 1], TRUE), Message("goal", live, TRUE)>> /\ online = TRUE /\ phase = "idle" /\ req = "none"
|
||||||
/\ invalid = FALSE
|
/\ invalid = FALSE
|
||||||
/\ ticket = [expired |-> FALSE, status |-> "active"]
|
/\ ticket = [expired |-> FALSE, status |-> "active", quota |-> FALSE]
|
||||||
/\ lastAct = [sent |-> FALSE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id], expected |-> ticket,
|
/\ lastAct = [sent |-> FALSE, view |-> [goal |-> w.goal.id, turn |-> w.turn.id], expected |-> ticket,
|
||||||
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE]
|
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE, usage |-> FALSE]
|
||||||
/\ known = {1} /\ need = {} /\ resumed = {} /\ cancelled = {}
|
/\ known = {1} /\ need = {} /\ resumed = {} /\ cancelled = {}
|
||||||
/\ stopWant = [id |-> 0, status |-> "paused", epoch |-> 0] /\ stopNeed = FALSE
|
/\ stopWant = [id |-> 0, status |-> "paused", epoch |-> 0] /\ stopNeed = FALSE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES QueueProgress
|
PROPERTIES QueueProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 2
|
TurnBound = 2
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 0
|
StampBound = 0
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ CONSTANTS
|
||||||
StampBound = 1
|
StampBound = 1
|
||||||
TurnBound = 1
|
TurnBound = 1
|
||||||
FailBound = 1
|
FailBound = 1
|
||||||
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset ApiEvidence NoEarlyResume StopCancels HumanStops
|
INVARIANTS TypeOK FreshRepair AutoProvenance SuccessReset FailureEvidence NoEarlyResume UsageEvidence StopCancels HumanStops
|
||||||
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
PROPERTIES RecoveryProgress StopProgress HoldProgress
|
||||||
CHECK_DEADLOCK FALSE
|
CHECK_DEADLOCK FALSE
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue