Timeline
A chat where something real got built is hard to read afterwards. Forty turns, hundreds of tool calls, and the answer to “where did that end up” somewhere in the middle of it. Scrolling back is not the answer — what you want is the shape of the work: asked, analysed, rendered, delivered. Four lines for four hours.
The timeline is that. It is not a summary: nothing is generated, nothing costs a model call to render, and every entry is a thing that actually happened, in the order it happened, with the outcome it had.
Two sources
Section titled “Two sources”Derived steps come from the turns themselves. One request, plus the work it caused, plus the answer it got, is one step — however many iterations it took. This runs on every chat, whether or not anything was ever declared, so a timeline is never empty.
Declared milestones come from the agent. When a phase genuinely ends, it says so with the
mark_milestone tool, and that step hangs under the request that produced it. Richer, and
optional: a forgotten call costs detail, never the whole view.
Outcomes are the point
Section titled “Outcomes are the point”Every entry carries a state, and the one that is neither done nor failed is why this exists.
| State | What it means |
|---|---|
done | It finished and it worked. |
failed | It finished and it did not. A tool errored, or the agent said so. |
open | It never finished. A request with no answer after it — a restart mid-turn, an engine that never came back, a tool that hung. |
running | It is being answered right now. |
superseded | You replaced it yourself: the same message sent twice, or another one seconds later. |
open is the one nothing in the product reported before. The chat simply stopped, and you had to
notice the absence yourself.
Why the last two exist
Section titled “Why the last two exist”Because without them open stopped meaning anything. Measured across 372 real conversations: of 39
steps with no answer after them, 28 were closed by another message from the same person, and 11
of those were the identical text sent again. A count that is mostly “you pressed send twice” is a
count people stop reading — which is the exact failure this feature was built to fix.
So a request its own sender replaced is superseded: still shown, so the step count matches the
chat, but greyed and counted as nothing.
running is the other half. Your request is written to the conversation before the model is
called, so on disk a chat being answered right now and one the daemon died inside look identical —
a request with nothing after it. The panel used to announce “never answered” over the very request
it was busy answering. Only the daemon’s register of live turns can tell the two apart, so that is
what decides this state.
Where to see it
Section titled “Where to see it”Inside a chat, as a collapsed rail above the transcript. The header line carries the counts worth interrupting for — how many steps, how many still open, how many failed — so if nothing is wrong you never open it. A short chat that went fine draws no rail at all.
Across chats, in the project’s Overview. This is the half the per-chat rail cannot answer: you do not know which conversation stalled. Filter to Unfinished only and what is left is the work somebody still has to do something about, whichever channel it arrived on.
mark_milestone
Section titled “mark_milestone”The agent records a step with one tool call. Three to six steps for an afternoon of work is right; thirty is noise.
| Parameter | Meaning |
|---|---|
title | The step in one short line, past tense — “Reel analysed”. |
state | done (default), open, failed, dropped. |
track | Optional group — the piece of work these steps belong to. |
detail | Optional couple of sentences a reader can expand. |
note | Optional note on the outcome. On failed, what went wrong. |
milestone | An id from a previous call, to close a step opened earlier. |
The default is done because a step is usually recorded once it is already over. Every call hands
back the chat’s still-open milestones, so a later turn can close one without a second tool to look
the ids up.
Storage
Section titled “Storage”Declared milestones live in ~/.apx/projects/<apxId>/milestones/YYYY-MM.jsonl, one file per month,
append-only — the same shape as tasks and commitments. State is the fold of the event stream.
They are deliberately not written into the message ledger: anything in there comes back as a conversation turn on the next prompt build, and a record about a conversation is not something anybody said in it.
Derived steps are stored nowhere. They are read from the transcript on request, which is what keeps them from ever disagreeing with it.
GET /api/projects/:pid/milestones?since=ISO&until=ISO&limit=NGET /api/projects/:pid/agents/:slug/conversations/:id/milestonesGET /api/projects/:pid/super-agent/threads/:channel/:id/milestonesPOST /api/projects/:pid/milestones { title, state?, track?, … }POST /api/projects/:pid/milestones/:id/close { state, note? }PATCH /api/projects/:pid/milestones/:id { patch: {...} }The cross-chat view is built from the ledger — one file per day — so a range costs its own days rather than every conversation the project has ever had. That is why it takes a range and not “everything”.