Skip to content
Guide

Context loss in AI coding agents, and the state layer that fixes it.

Context loss, for an AI coding agent, is the gap between what the agent knew when a session ended and what the next session knows when it starts — the decisions made, the work deferred, the rule learned the hard way, the branch it was on. Session continuity is a state problem, not a window-size problem.

By PostMQ · published 2026-08-18 · last verified 2026-09-03 against main at 2ececc05

What context loss is, and why it happens

The context window ends, or is compacted, and the next session begins from the repository plus whatever prose someone thought to leave behind. That is the whole of the problem, and it is not solved by holding more of the last session in memory — it is solved by writing the parts that matter somewhere the next session can query.

It happens for four ordinary reasons. The window is finite, so a long session ends mid-thought. A compaction or a summary is a compression, and a compression drops what did not seem important at the time. Memory files are unstructured, so a new session cannot tell what was decided from what was merely discussed, or which line is current and which is a month old. And a new session on a second machine, or a second branch, or under a second agent, has no way to know that another session on the same project already exists.

The fix that holds is not a bigger window. It is an explicit state layer the agent writes to and reads from: a build session with a start, a ping, a continuation link and a current-state lead, so the next session resumes rather than reconstructs; and the decisions in it go into an append-only decision log, not a summary the next session may skip. The rest of this guide is that layer, piece by piece — first the pattern, then how each piece is shipped, then a session run against it, and how to check it worked.

The hand-rolled fixes, and where each breaks

Every team that runs coding agents for more than a week builds one of three things, in this order. Each is a real improvement over nothing, and each breaks in a specific place. None of them names a vendor here — they are patterns, and the point is the shape of the failure, not who ships which.

A state file in the repo. One markdown file the agent rewrites in place with what it was doing and what comes next. It breaks because it is rewritten: yesterday's reasoning is gone the moment today's replaces it, so there is no way to ask why something is the way it is. It lives on one machine and one branch. And nothing in the file says who wrote which line, or when.

An end-of-session summary. A paragraph the agent writes when it stops, pasted into the next prompt. It breaks because a summary is a compression: what was decided and what was merely discussed land in the same paragraph, in the same register, and a decision that did not fit the paragraph is lost silently — the next session does not know there was something it did not read.

A hook that re-injects. A script that prints the file or the summary into every new session. It breaks because it re-injects whatever the file says, stale or not, in full: the reading grows with the project, and nothing filters it to the change at hand, so the rule about locking order arrives when the agent is editing a stylesheet.

The hand-rolled fixes for context loss, what each holds, where each breaks, and what the state layer does instead
approachwhat it holdswhere it breaksthe state layer instead
A state file in the repo One markdown file the agent rewrites in place: what it was doing, what is next. It is rewritten, so yesterday’s reasoning is gone; it lives on one machine and one branch; nothing says who wrote which line, or when. A build session per project, computer, branch and actor, resumed by a unique index; a rolling current-state lead where the prior lead is demoted, not overwritten; every write attributed to the credential that made it.
An end-of-session summary A paragraph the agent writes when it stops, pasted into the next prompt. A summary is a compression: what was decided and what was merely discussed land in the same paragraph, and a decision that did not fit is silently lost. A history entry the session ends with, plus one append-only decision-log entry per decision — a trigger rejects edits and deletes, and a correction is a new entry that points at the old one.
A hook that re-injects A script that prints the file or the summary into every new session. It re-injects whatever the file says, stale or not, in full — the reading grows with the project and nothing filters it to the change at hand. A start that returns the active session with its lead; rules asked for by the file paths, operations and languages of the change, so only the matching ones come back.
A bigger context window Everything, until it does not. The window still ends; a compaction is another summary; and nothing about a larger buffer tells the next session what was decided, deferred or consumed. A record the next session queries — sessions, decisions, backlog, usage — held outside the window and outside the client.

Note what the last column does not say. It does not say the file, the summary or the hook are wrong — we keep convention files in the repo too, and a hook that prints drift signals at every start. It says the record the next session needs is not a compression of the last one; it is a set of typed rows the next session queries, held outside the window and outside the client. Nothing here changes what your client keeps for itself; the layer sits beside it.

The explicit state layer

The pattern is five nouns and a rule. A session that is the same object every time the same work resumes. A current-state lead the next session reads first. A decision log that only grows. A backlog for what was deferred, and a usage record for what was consumed. The rule: the agent asks the layer before it acts and writes to it as it goes — not once, at the end, from memory. Here is how each piece is shipped in PostMQ, with the tool that does it. PostMQ speaks standard MCP, so any MCP-capable client can make these calls; the names below are the server's own, each linked to its row in the catalogue.

The session loop, with a message crossing to another agent Six steps on a rail: start-session, rules, work, decisions, end-session, usage, joined back into a loop. From the work step an envelope leaves the rail to a second lane labelled another agent, where it lands in an inbox tray and moves through pending, in_flight and acknowledged, the last marked with a filled amber tick. 1 start-session start_build_session 2 rules query_applicable_rules 3 work send · get_pendingack 4 decisions append_decision_logfile_lessonfile_backlog_item 5 end-session end_build_session 6 usage record_build_session_usage another agent pending in_flight acknowledged
The loop the state layer sits under: start a session, ask which rules apply, work, log the decisions and the lessons learned, end, report usage — and hand the results on as a durable message another agent, or a later session, picks up and acts on.

A session that resumes, not restarts

start_build_session is resume-or-create: the same project, computer, branch and actor gets its active session back (200) — otherwise a fresh one starts (201). A unique index on exactly that tuple makes it true, so two concurrent starts cannot mint two active sessions and a reconnect after a crash lands on the session it left. Where the tuple cannot catch a resume — the same work continued on a second machine, or carried onto a new branch — link_build_session_continuation records the predecessor explicitly: it refuses a self-link and a cycle, is idempotent when the link already exists, and the dashboard shows it as Continues. A ping_build_session at start and at natural pauses stamps last_pinged_at with a single UPDATE that never moves the stamp backwards; a closed session is a no-op. Read that honestly: the stamp is a liveness record the dashboard shows as Last ping, and no sweep reads it today — nothing expires an unpinged session.

A current-state lead the next session reads first

The session carries a rolling current_state_markdown. prepend_build_session_lead puts a new lead paragraph at the top and demotes the prior lead with an **Earlier:** prefix rather than overwriting it — idempotent, so an already-demoted lead is not prefixed twice — and it reads the row under an update lock held to commit, so two agents prepending at once both keep their leads. update_build_session_state is the wholesale replace, for when the caller composes the whole value. Either way the value is capped at 32,000 characters and a closed session refuses the write. Neither is audited: a rolling note is not a lifecycle event.

"Reads first" is a fact about the reader, not the server. list_build_sessions and get_build_session return the whole session — the current state, the closing history entry, the last ping, the continuation link — and in the recipe we run, the first call a session makes is a list of the active sessions, before it touches a file. The server returns the state; the client decides to read it before anything else, and ours does.

Decisions into an append-only log, not a summary

Each material decision is one append_decision_log — a title, a rationale in markdown, the decision's logical date, whether it changes a public contract. If the same agent has a session open, the entry links to it and takes the project from it, without being told to. The log is append-only by a database trigger: every delete, and every update except the two source links, is rejected beneath the service layer. A decision that turns out to be wrong is not edited; a correction is a new entry that points at the one it supersedes, and the reader sees both. This is the piece that separates decided from discussed — a summary cannot, because it has no row per decision.

Deferrals into a triaged backlog

What was not done becomes one item in the backlog, filed with file_backlog_item — a title, a body, a priority, a category — in the open state and, like a decision, scoped to the open session's project. Later it is triaged, assigned or reprioritised; resolving needs a closing PR, commit or closing notes, dismissing needs a reason, and both are final. "I'll do it later" without a row is the failure this replaces.

Rules asked for at decision time

Before it touches a file, the agent calls query_applicable_rules with the file paths, operations, languages, code patterns and project attributes of the change, and gets back only the rules whose every populated trigger dimension matches. That is the answer to the re-injecting hook: the rule about locking order comes back when the change is a locking read, and stays out of the way when it is a stylesheet.

An end that leaves a history entry, and a usage record

end_build_session sets the end instant, persists the closing history entry the next session reads, and stamps the pull requests and commit SHAs the session produced. Re-ending a closed session returns it unchanged — no second audit row, no re-stamp. Then record_build_session_usage records what the session consumed: five token classes and turns per transcript, keyed on the transcript id so a re-report overwrites and never double-counts. Token counts, not dollars, and self-reported — by the agent, or by the hook described below.

A session, start to end

Prerequisites. A workspace, and an agent connected to it — a client that implements MCP authorization asks for its own credential and you approve it once; where there is no browser, issue one in the mcp_config form instead, scoped and once-disclosed. The steps are on the docs page. What follows is the protocol as this repository runs it, wired as /start-session and /end-session in Claude Code. It is how we run it, a recipe: the server does not enforce the order, and any client can make the same calls.

What a session reads at start. A hook prints the drift signals first — the protocol reminder, the age of origin/main's last commit, the active worktrees, and every open pull request with its CI rollup and merge state. Then the start skill queries the layer: the active sessions (the resume target, with their current-state leads), the last ten decisions, and the backlog counts by status. Only then does it pick a worktree and a branch and call start_build_session — resumed if this project, computer, branch and actor already have one — and ping_build_session it. The session id is written to a state file so the usage hook can find it at the end.

What it writes as it goes. A pull request is recorded with record_build_session_prs at the moment it is opened, not saved up for the close. The forge, the number and the repository are read off the PR's url; a linked issue or work item is recorded by supplying its own url alongside, since a PR url does not name what it closes. That timing is the point: a session that crashes, exhausts its context, or is simply abandoned never reaches its close, and a close-time-only record loses exactly the sessions whose work is hardest to find again.

What it writes at end. One append_decision_log per material decision. One file_backlog_item per deferral, and a triage to resolved for what was finished. Then end_build_session with the history entry, the PRs and the commits — and the work commit carries the session id as a trailer, so a reader can go from a commit to its session and to the decisions it logged. When the client exits, a SessionEnd hook reads the transcript and calls record_build_session_usage against the session named in the state file; the numbers are self-reported by the hook, and idempotent per transcript.

two of the writes
prepend_build_session_lead
{
  "method": "tools/call",
  "params": {
    "name": "prepend_build_session_lead",
    "arguments": {
      "build_session_id": "01K…",
      "new_lead_markdown": "Sweep loses to a live lease; ack refuses a re-pended row. Next: the crossing-boundary case, then the guard test."
    }
  }
}
end_build_session
{
  "method": "tools/call",
  "params": {
    "name": "end_build_session",
    "arguments": {
      "build_session_id": "01K…",
      "session_history_entry_markdown": "Lease sweep loses to a live lease; ack refuses a re-pended row. PRs verified at close: #521 MERGED.",
      "related_prs": [{ "url": "https://github.com/…/pull/521" }],
      "related_commit_shas": ["f6d1374"]
    }
  }
}

Real argument names, placeholder values — the shapes, not a transcript.

Dogfooded. This repository is built this way, and the numbers below are its own — generated from the project's ledger and git history at build, never typed; the dogfood check fails the build when the file goes stale.

914of 944 commits on main carrying a session trailer
528 backlog items · 86 open · 438 resolved

As of 2026-09-02 — the project's own ledger and git history, read at build.

Verify it worked

Three places to look. First, the dashboard: app.postmq.com/build-sessions lists the workspace's sessions — branch, computer, status, started, intent — with a search over intent, notes and history; app.postmq.com/build-sessions/{id} shows the overview (status, computer, branch, worktree, intent, client, started, Last ping, ended, and Continues when a continuation is linked), the Current state with its lead on top and the demoted leads under it, the Session history entry, and what the session Produced — the pull requests and commits. The same page can reassign the session to a project and end it; everything else is read-only. Second, app.postmq.com/decision-log, newest first with a project filter and a search over title and rationale: the entries you appended carry the session they were made in. Third, from the agent itself: get_build_session_aggregate returns the session with its project and its continuation predecessor in one call, so a resuming agent can prove it landed on the session it meant to.

Every lifecycle event in this layer — sessions started and ended, continuations linked, decisions appended and corrected, backlog filed and triaged — is a row on the workspace's tamper-evident SHA-256 audit chain, hashed in the same transaction as the row it records: 38 of 123 event types. Pings, lead updates and usage reports are deliberately not audited. How the chain works, and what is not built, is on the security page.

When not to use this

Be candid about the fit. A single short task — one prompt, one file, done in a sitting — needs no session: the window holds it and there is nothing to resume. If all you want is a scratchpad the agent can read back, a file in the repo is the right tool and the cheaper one. A solo developer on one machine and one branch loses little to the state file's blind spots — one author, one line of history — and gains the layer's value only when a second machine, a second branch, a second agent, or a reviewer who was not in the session shows up. And the layer is written by the agent: a client that never calls append_decision_log gets an empty log, not a bad one. The record is only as good as the recipe that writes to it, which is why ours runs at every session boundary and not on request.

Next

Session state — the nine-step protocol, the guarantees table and the API surface for every piece above. The decision log — the trigger, the correction shape, the nine tools and nine routes. Claude Code — the .mcp.json entry, the two skills and the two hooks we run. How we run start-session and end-session — the two skills step by step, as this repository runs them, with the numbers. The MCP server — every tool named here, in the catalogue. The docs — connect an agent. Security — the audit chain, credentials, and what is deliberately not built.

Last verified 2026-09-03 against main at 2ececc05: the build-session service, its tools, the two skills, the two hooks and the two dashboard pages this guide names were read on that day.

Related: session state — the protocol this guide describes · the decision log — append-only, corrections as new entries · the Claude Code setup we run · the MCP server and its tool catalogue · connect an agent · the audit chain and what is not built.

Start a session. The next one resumes it.

Create a workspace, connect your agent, call start_build_session.