Fix goal recovery

This commit is contained in:
ms2k 2026-10-22 22:05:33 +09:00
commit 996f586bc1

View file

@ -1,26 +1,33 @@
------------------------------- MODULE Keep -------------------------------
EXTENDS Naturals, Sequences
\* Each connection serializes RPCs; one abandoned write may finish after reconnect. Epochs audit human intent only.
\* Host-authored controls form an append-only input stream; progress assumes eventual connectivity and fair finite work.
\* Each connection serializes RPCs; one abandoned write may finish after reconnect. Epochs audit explicit intent only.
\* 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
GoalStates == {"active", "blocked", "paused", "complete", "cleared",
"budgetLimited", "usageLimited"}
TurnStates == {"inProgress", "failed", "completed", "interrupted"}
Reads == {"pollGoal", "pollTurn", "thread", "check", "attach"}
Reads == {"pollGoal", "pollTurn", "thread", "check", "attach", "usage"}
Events == {"goal", "start", "done"}
Episodes == (1..GoalBound) \X (1..TurnBound)
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}
/\ StampBound \in Nat /\ TurnBound \in Nat \ {0}
/\ FailBound \in Nat \ {0}
Recon == Mode \in {"Observe", "Unordered", "BarrierOld", "Reconcile"}
Durable == Mode \in {"Unordered", "BarrierOld", "Reconcile"}
Ordered == Mode \in {"BarrierOld", "Reconcile"}
Recon == Mode \in {"Observe", "Unordered", "BarrierOld", "Filtered", "BlindUsage", "Reconcile"}
Durable == Mode \in {"Unordered", "BarrierOld", "Filtered", "BlindUsage", "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,
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,
fails : 0..FailBound, due : {"none", "waiting", "ready"},
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,
turn : TurnType, truth : 0..EditBound, busy : BOOLEAN, ok : BOOLEAN]
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,
ticket, lastAct, known, need, resumed, cancelled, stopWant, stopNeed
@ -49,17 +56,20 @@ Message(kind, state, ok) ==
Stop(x) == [x EXCEPT !.armed = FALSE, !.due = "none"]
Increment(x) == [x EXCEPT !.fails = IF @ < FailBound THEN @ + 1 ELSE @]
Eligible(x) == x.armed /\ x.goal.id # 0 /\ x.goal.status = "blocked"
/\ x.turn.status = "failed" /\ x.turn.api
Retry(t) == t.api \/ AllFailures
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"
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
BaseGoal(x, g) ==
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 = "blocked" /\ x.due # "none"
\/ (g.status \in Waiting /\ x.due # "none"
/\ x.goal.stamp # g.stamp)
base == [x EXCEPT !.goal = g, !.fails = IF same THEN @ ELSE 0,
!.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
LET same == g.id # 0 /\ g.id = x.goal.id
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
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,
!.armed = IF active THEN @ \/ transition ELSE IF stopped \/ g.id = 0 THEN FALSE ELSE @,
!.due = IF transition \/ stopped \/ g.id = 0 THEN "none" ELSE @,
!.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
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
THEN [Stop(x) EXCEPT !.goal = g, !.hold = IF Durable /\ g.status # "active" THEN @ ELSE "none",
!.armed = g.status = "active",
@ -96,11 +107,12 @@ TurnUpdate(x, t) ==
started == [base EXCEPT !.armed = (Recon /\ x.armed)
\/ (t.status = "inProgress" /\ x.goal.id # 0 /\ x.goal.status = "active")]
terminal == t.status = "interrupted"
\/ (t.status = "failed" /\ ~t.api)
\/ (t.status = "failed" /\ ~Retry(t))
\/ (t.status = "completed" /\ ~Recon)
settled == IF terminal THEN Stop(base) ELSE base
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"
THEN [settled EXCEPT !.armed = TRUE] ELSE settled
held == IF Recon /\ t.status = "interrupted" /\ x.goal.id # 0 /\ x.pending
@ -112,7 +124,7 @@ TurnUpdate(x, t) ==
ApplyInput(x, status) ==
IF status = "cleared" THEN [Stop(x) EXCEPT !.hold = "none", !.known = FALSE] 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"]
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]
/\ wire = <<>> /\ online = TRUE /\ phase = "idle" /\ req = "none"
/\ 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,
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE]
origin |-> "automatic", evidence |-> TRUE, recover |-> TRUE, usage |-> FALSE]
/\ known = {1} /\ need = {} /\ resumed = {} /\ cancelled = {}
/\ 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,
ticket, lastAct, known, need, resumed, stopWant, stopNeed>>
FailTurn(isApi, stamp) ==
FailTurn(isApi, status, stamp) ==
/\ live.turn.status = "inProgress" /\ live.turn.id <= TurnBound
/\ LET g == IF live.goal.status = "active"
THEN [live.goal EXCEPT !.status = "blocked",
THEN [live.goal EXCEPT !.status = status,
!.stamp = stamp]
ELSE live.goal
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
IN /\ live' = state
/\ 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
/\ UNCHANGED <<late, inputs, sample, attached, w, online, phase, req, invalid,
ticket, lastAct, known, resumed, cancelled, stopWant, stopNeed>>
@ -224,7 +236,7 @@ Poll ==
known, need, resumed, cancelled, stopWant, stopNeed>>
ServeRead ==
/\ req \in Reads
/\ req \in Reads \ {"usage"}
/\ wire' = IF ~online THEN wire ELSE
IF req = "attach" THEN Append(Append(wire, Message(req, live, TRUE)), Message("goal", 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,
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) ==
IF invalid /\ ~(Mode = "BarrierOld" /\ m.kind = "attach")
THEN IF Ordered /\ m.kind = "attach"
@ -246,6 +266,7 @@ ReadWatch(m) ==
IN IF Recon /\ ~attached /\ m.goal.id # 0
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 = "usage" -> w
[] m.kind = "pollTurn" -> Schedule(TurnUpdate(w, m.turn))
[] m.kind = "thread" -> IF ~m.busy THEN w ELSE
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
THEN "thread" ELSE "idle"
[] 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 ==
/\ 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 @]
/\ phase' = next
/\ req' = IF next \in Reads THEN next ELSE "none"
/\ ticket' = IF next = "send"
THEN [expired |-> w.due = "ready", status |-> "active"]
ELSE ticket
/\ ticket' = IF m.kind = "usage" THEN [ticket EXCEPT !.quota = m.ok]
ELSE IF next \in {"send", "usage"}
THEN [expired |-> w.due = "ready", status |-> "active", quota |-> FALSE]
ELSE ticket
/\ known' = IF ~invalid /\ m.kind \in {"pollGoal", "check"}
THEN KnownAfter(m.goal) ELSE known
/\ sample' = IF Head(wire).kind = "pollGoal" /\ ~invalid THEN Head(wire).goal ELSE sample
@ -283,7 +307,7 @@ ReceiveRead ==
/\ attached' = (attached \/ Head(wire).kind = "attach")
/\ LET m == Head(wire)
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
IN /\ stopWant' = IF stopped THEN [id |-> m.goal.id, status |-> m.goal.status, epoch |-> m.truth] ELSE stopWant
/\ stopNeed' = (stopNeed \/ stopped)
@ -293,7 +317,7 @@ ReceiveRead ==
ReadInput ==
/\ online /\ phase = "inputs" /\ req = "none"
/\ 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,
!.known = TRUE, !.armed = TRUE] ELSE updated
repair == ready.hold # "none" /\ ready.hold # sample.status
@ -316,7 +340,7 @@ ReceiveEvent ==
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
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"))
base == IF seal /\ Recon THEN [w EXCEPT !.fence = Len(inputs)]
ELSE IF Ordered \/ (ignore /\ ~echo /\ ~snapshot) THEN w ELSE SyncInput(w)
@ -327,12 +351,12 @@ ReceiveEvent ==
ELSE IF ignore THEN w ELSE Schedule(updated)
EXCEPT !.seed = IF seed THEN EmptyGoal ELSE @]
/\ 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]
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
/\ 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))
/\ invalid' = (invalid \/ ((~ignore \/ (~Ordered /\ (echo \/ snapshot) /\ NewInput(w))) /\ phase # "idle"))
/\ wire' = Tail(wire)
@ -344,11 +368,11 @@ SendSet ==
/\ IF phase = "send" THEN Eligible(w) ELSE w.hold # "none"
/\ phase' = "set" /\ req' = "set" /\ invalid' = FALSE /\ w' = [w EXCEPT !.pending = TRUE]
/\ 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],
expected |-> IF phase = "send" THEN ticket ELSE [ticket EXCEPT !.status = w.hold],
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>>
ServeSet(stamp) ==
@ -371,7 +395,8 @@ ReceiveSet ==
base == [w EXCEPT !.pending = FALSE, !.echo = IF m.ok /\ Recon THEN m.goal ELSE @]
IN /\ w' = IF m.ok
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
/\ known' = IF m.ok /\ ~invalid THEN KnownAfter(m.goal) ELSE known
/\ phase' = "idle" /\ invalid' = FALSE /\ wire' = Tail(wire)
@ -390,7 +415,8 @@ Disconnect ==
/\ req' = "none"
/\ late' = IF req = "set" THEN ticket.status ELSE late
/\ 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]
/\ 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)
\/ StartTurn
\/ \E status \in {"completed", "interrupted"} : FinishTurn(status)
\/ \E isApi \in BOOLEAN, stamp \in live.goal.stamp..StampBound : FailTurn(isApi, stamp)
\/ Poll \/ ServeRead \/ Receive \/ SendSet \/ ReadInput
\/ \E isApi \in Failures, status \in Waiting, stamp \in live.goal.stamp..StampBound : FailTurn(isApi, status, stamp)
\/ 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 : ServeLate(stamp))
\/ Clock \/ Disconnect \/ Reconnect \/ HonourStop
@ -442,12 +468,13 @@ TypeOK ==
/\ stopWant \in StopType /\ stopNeed \in BOOLEAN
FreshRepair == phase # "repair" \/ ~invalid
ApiEvidence == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.evidence
FailureEvidence == ~lastAct.sent \/ ~lastAct.recover \/ lastAct.evidence
AutoProvenance == ~lastAct.sent \/ lastAct.origin = "automatic"
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
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")
@ -466,5 +493,6 @@ FairSet == \E stamp \in live.goal.stamp..StampBound : ServeSet(stamp)
Fairness == WF_vars(Poll) /\ WF_vars(ServeRead) /\ WF_vars(Receive)
/\ WF_vars(SendSet) /\ WF_vars(FairSet) /\ WF_vars(Clock) /\ WF_vars(StartTurn)
/\ WF_vars(FairLate) /\ WF_vars(HonourStop) /\ WF_vars(ReadInput)
/\ SF_vars(ServeUsage(TRUE))
Spec == Init /\ [][Next]_vars /\ Fairness
=============================================================================