Track which attributed reply changed a decision without letting quoted history or message order alone overwrite active facts.
Email thread state: corrections, commitments and stale claims
Model the decision as a ledger
A support thread can contain a proposed maintenance window, a correction, an acknowledgement and a later cancellation. Summarizing all four as one current plan is wrong. Record each authored claim as an event with message ID, sender, field, value, intent and timestamp. Link corrections to the claim they supersede. Quoted history remains attached to its original event. Quote attribution must run before the ledger is built.
Require explicit correction scope
The words “change it to 03:47” only update a window if “it” can be resolved to the relevant proposal. A later email about another service must not overwrite the first service’s state. Define a subject key from trusted ticket ID and service ID. A correction must name or unambiguously refer to a prior field. Otherwise, retain both claims and ask a reviewer. Conversation repair gives a broader model for unresolved references.
Separate evidence from authorization
A customer may request a different time; an operator may accept it; a scheduler may still need a formal approval. The ledger should represent these as different states rather than infer approval from recency. Role comes from authenticated mail identity and workflow policy, not a signature block inside the message. Avoid silently accepting an attached forwarded message as authority. Trusted action gates cover the move from text interpretation to external action.
Evaluate state at cutoffs
Ask what was known after each message, not only at the end of the thread. Test out-of-order delivery, duplicate Message-IDs, late forwards, withdrawn commitments and two parallel subjects. Measure correction-link accuracy, active-state accuracy and unsafe approval assertions. An automatic summary can be helpful even when action is held. The project implements a conservative release check.
Implementation
def current_window(events, ticket_id):
active = None
for event in events:
if event["ticket_id"] != ticket_id or event["provenance"] != "authored":
continue
if event["field"] != "window_utc":
continue
if event["intent"] == "propose":
active = {"state": "proposed", "value": event["value"],
"message_id": event["message_id"]}
elif event["intent"] == "correct" and active and event["supersedes"] == active["message_id"]:
active = {"state": "corrected", "value": event["value"],
"message_id": event["message_id"]}
return active or {"state": "unresolved"}
thread = [{"ticket_id": "ticket-47", "provenance": "authored",
"field": "window_utc", "intent": "propose", "value": "02:47Z",
"message_id": "mail-47", "supersedes": None},
{"ticket_id": "ticket-47", "provenance": "authored",
"field": "window_utc", "intent": "correct", "value": "03:47Z",
"message_id": "mail-82", "supersedes": "mail-47"}]
assert current_window(thread, "ticket-47")["value"] == "03:47Z"
assert current_window(thread[:1], "ticket-47")["value"] == "02:47Z"
Performance and operating cost
For m events, the scan takes O(m) time and O(1) extra space. It assumes an already verified event order, unique message IDs and resolved correction targets. Production handling must detect duplicates, out-of-order arrivals and conflicting parallel proposals before relying on an active state.
Common Mistakes
- Letting a quoted older time replace a fresh correction.
- Treating the last email as approved simply because it is last.
- Applying a correction to another ticket or service.
- Flattening event history so reviewers cannot reconstruct the state at a cutoff.
Read next
- Email threads: quote boundaries and authored-text attribution
- Project: reconstruct a support email decision ledger
- Conversation repair: corrections, clarification and unresolved targets
- Speech acts: requests, reports and commitments in support dialogue
- Trusted action gates for retrieval-assisted answers
