bfinn’s LessWrong post describes the Third Time workflow several ways: with examples, with natural language pseudocode procedures, with visual aids, … I think it’s great and I’ve used it to implement various Third Time GUIs.
Now that it’s 2025, it only makes sense for me to figure out how to get AI agents to write the GUIs instead. But when I try, I find myself arguing with them a lot.
Third Time is just complex enough that an AI can write a design doc about it, and I can approve that doc, but I can still disapprove of the implementation because we disagree on how to handle some edge case. When faced with ambiguity, I often see AIs make technocentric assumptions that simplify the code but make the software less usable, like:
- If the user overruns a break, their accrued break time goes negative
- The user is always working or resting
I’ve been experimenting with conceptual models as a solution. I read about them pre-AI in Daniel Jackson’s book “The Essence of Software” and loved the idea, but couldn’t get my software team on board. My team at work still isn’t on board, but after hours, I find that AI is easier to convince to try something new.
The structure of a conceptual model forces edge cases to the fore in the same way that writing code does, but is general enough to describe software in an implementation-independent way. It supports state changes, but is flexible enough to support timeless aspects of a system, like invariants.
Conceptual models are designed to capture the way users think about the software. For example, the book decomposes Gmail into “Message” and “Thread” and “Label” concepts whose relationships are crisply defined, then explains usability issues as either miscommunication about the underlying concept (do you know if labels apply to Messages or Threads? what about Stars?), or deviations that the implementation make from the underlying concept.
Anyway, long story short, when I collaboratively wrote a conceptual model for Third Time instead of a free-form design doc, AI finally understood what I meant.
I thought people here might find my collaboratively-written version of that model useful:
Concept: TimeSegment
A TimeSegment
is a fundamental, indivisible, and historical record of a single, continuous activity. It is the atomic building block of a session’s history.
Signature
-
Properties:
type
: The kind of activity that occurred during this segment (Work
orRest
).start
: The instant in time when the segment began.end
: The instant in time when the segment concluded.
-
Constraints (Invariants):
- The
end
instant must occur at or after thestart
instant.
- The
Concept: TimeLedger
A TimeLedger
is a chronologically ordered history of recorded work and rest activities. It is the single source of truth from which all current states are derived.
A ledger is intended to be ephemeral and can be discarded when its accumulated history is no longer relevant, such as at the end of a work day. There is no expectation that accumulated rest counters have any use across long periods of inactivity.
Signature
-
Relations:
segments
: An ordered list ofTimeSegment
individuals, sorted chronologically.
-
Derived Properties (as functions of time):
availableRest(t)
: A function that returns the available rest time at a given instantt
.overRestTime(t)
: A function that returns the total accumulated over-rest time at a given instantt
.
-
Formal Definition of Derived Properties:
The values are defined recursively as piecewise functions over the ledger’s segments.- Base Case: For any time
t
before the start of the first segment,availableRest(t)
andoverRestTime(t)
are both 0. - Recursive Step: The values for any time
t
after the start of the first segment are defined based on the segment or gap it falls into.- If
t
falls within a segments
:- If
s
is aWork
segment:availableRest(t) = availableRest(s.start) + (t - s.start) / 3
overRestTime(t) = overRestTime(s.start)
(Over-rest time is not paid down by working.)
- If
s
is aRest
segment:availableRest(t) = max(0, availableRest(s.start) - (t - s.start))
overRestTime(t) = overRestTime(s.start) + max(0, (t - s.start) - availableRest(s.start))
- If
- If
t
falls between two segments or after the last segment: Its value is the same as the value at the end of the preceding segment.
- If
- Base Case: For any time
-
Constraints (Invariants):
segments
in the ledger must not overlap in time.
Concept: Timer
(The Controller)
The Timer
is an interactive, real-time device. Its state represents a tentative, uncommitted segment that has a start time but whose end time is always the present moment.
A user interface for a Timer will typically show ledger.availableRest(now)
for a ledger that includes the Timer’s tentative segment.
- Signature:
state
: The current interactive state (Paused
,Working
, orResting
).activeSegmentStart
: A timestamp marking when the current, in-progress segment began.