Skip to content
Session state

Every session starts where the last one stopped.

An AI coding session is a record, not a chat. It resumes where the same project, computer, branch and actor left off, asks which rules apply before it touches a file, logs what it decided, files what it deferred, ends with a history entry and reports what it consumed — over one MCP server, a REST API and a dashboard.

build session

start_build_session → 200 resumed
A build session A bracket from start to end with a usage bar beneath, filled to about two thirds. start end

current state · lead

Lease sweep loses to a live lease; ack refuses a re-pended row. Next: the concurrent crossing-boundary case, then the guard test.

usage · 84.2k tokens · token counts, not dollars · illustrative

How it works

Ask before you act. Record what you did.

Nine tool calls, in the order an agent makes them. This is the protocol this repository runs on itself, every session.

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: 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.
  1. Start — or resume

    The same project, computer, branch and actor gets its active session back (200); otherwise a fresh one starts (201). A unique index makes that true, so a reconnect never mints a duplicate.

    start_build_session
  2. Ask which rules apply

    Pass the file paths, operations, languages, code patterns and project attributes of the change; only the rules whose every populated trigger dimension matches come back, ranked by how often they have been retrieved.

    query_applicable_rules
  3. Work, and keep the lead current

    The session carries a rolling current-state lead — the paragraph the next session reads first. A cross-machine resume the tuple cannot catch is linked explicitly as a continuation.

    prepend_build_session_lead
  4. Record each material decision

    One entry per decision, auto-linked to the open session. The log is append-only by a database trigger; a correction is a new entry that points at the one it supersedes.

    append_decision_log
  5. File each deferral

    Anything not done becomes an open item with a priority and a category. It is later triaged, assigned or reprioritised; resolving needs a closing PR, commit or notes, dismissing needs a reason, and both are final.

    file_backlog_item
  6. End with a history entry

    Sets the end instant and persists the closing history entry, plus the pull requests and commits the session produced. Re-ending a closed session returns it unchanged.

    end_build_session
  7. Report what it consumed

    Five token classes and turns per transcript, keyed on the transcript id so a re-report overwrites and never double-counts. A Claude Code SessionEnd hook does this for you.

    record_build_session_usage
  8. Hand work to another agent

    A review, a test run — anything another agent must do leaves as a typed message and waits in that agent’s inbox until its next get_pending. Recipients are AI accounts in the same workspace.

    send
  9. Let lessons graduate into rules

    Recurring pain is filed as a lesson and moves forward only — observed, documented, enforced. A rule is derived only from an enforced lesson, and step 2 returns it next time.

    derive_rule

A build session opens with start_build_session — resumed if this project, computer, branch and actor already have one, enforced by a unique index. Before touching a file the agent calls query_applicable_rules with the files, operations and languages it is about to change and gets back only the rules that match. It works. It writes append_decision_log for each material decision and file_backlog_item for each deferral. It closes with end_build_session and reports record_build_session_usage. Anything that needs another agent — a review, a test run — leaves as a send and waits in that agent's inbox until its next get_pending. Each tool links to its row in the catalogue; the three-step setup is on the docs page, and the skills and hooks we run it with are on the Claude Code page. Why a session needs a state layer at all — what context loss is, and where a state file, a summary or a re-injecting hook breaks — is the context-loss guide; how we run the two skills ourselves, step by step, is the start-session and end-session guide. None of this displaces the memory your coding client keeps for itself; the two hold different things, and which is which is set out row by row, cited on both sides, on PostMQ and Claude Code auto memory.

Session bracket A bracket labelled start and end with a usage bar beneath. start end A rule badge A pill reading rule/lock-order — a rule returned by query_applicable_rules. rule/lock-order
Steps 1–2 — start, then ask. The bracket is the session: start, end, the usage bar beneath. Same project, computer, branch and actor → the same bracket, resumed; the badge is a rule that came back.
Decision-log entry A ledger row with a timestamp, a decision pill and a text stub. 14:02:11 decision Second decision-log entry A second ledger row with a later timestamp. 15:47:03 decision A correction entry A third ledger row whose pill reads corrects — a new entry pointing at the first. 16:10:58 corrects
Step 4 — record. Ledger rows are appended, never rewritten; the third is a correction pointing at the first.
Backlog columns Three narrow columns of tiles; one tile carries a hollow amber tick for stale.
Step 5 — defer. Open, in progress, resolved; the hollow amber tick is a stale item nobody has acknowledged yet.
An agent asks which rules apply and gets three back, ranked A session bracket labelled agent sends query_applicable_rules with an area and a topic; three rule badges come back in rank order, the first marked with an amber tick. start agent · build session query_applicable_rules { area: "sql", topic: "locks" } 3 rules, ranked 1 rule/lock-order 2 rule/typed-params 3 rule/cross-key-test a rule is a promoted lesson: it answers before the agent acts, not after it fails
Step 2 — ask, in full. The session asks with the change it is about to make; three rules come back, ranked by how often they have been retrieved.
A lesson becomes a rule: candidate, two observations, promotion, one badge Four ledger rows on the left — a lesson candidate, two observations of the same shape, and a promotion — converge with a bracket into a single rule badge on the right, marked enforced with an amber dot. day 1 lesson a race test that hopes to land in the window is not a guardrail day 3 observation same shape, second suite day 9 observation same shape, review round day 12 promoted human gate · status: enforced rule/lock-order returned by query_applicable_rules retrieval counted on every query a rule is never invented — it is a lesson that was seen again and promoted through a human gate
Step 9 — learn. A candidate, two observations of the same shape, a promotion — one rule, returned by the next query. The gate is structural: enforced requires a written prevention mechanism.
What it holds

Nine ledgers, one project scope

Every ledger — sessions, decisions, backlog, lessons, rules, usage — is scoped to a project inside your workspace, and each card names the object it is made of.

Resume-or-create sessions

The same project, computer, branch and actor gets the same active session back — 200 on resume, 201 on create — enforced by a unique index rather than a client-side check, so two concurrent starts cannot mint two active sessions.

index:UQ_build_sessions_active

Current-state lead and continuation

A rolling current-state lead the next session reads first; the prior lead is demoted, not lost. A session can be linked as the continuation of one on another machine or branch, and it ends with a history entry.

tool:prepend_build_session_lead

Append-only decisions

Decisions cannot be edited or deleted — a database trigger rejects deletes and every update except source links. Corrections are new entries that point at the entry they supersede; the original is never edited.

trigger:tg_decision_log_entries_append_only

Backlog with triage and staleness

File, triage, prioritise, assign, reprioritise; staleness is a persisted instant, acknowledged explicitly. Resolve requires a closing PR, commit or closing notes; dismiss requires a reason; resolved stays resolved.

tool:file_backlog_item

Rules by context

Describe the change — file globs, operations, code patterns, languages, project attributes — and get back only the rules whose every populated trigger dimension matches. Each rule shows how often it has been retrieved, and the dashboard replays the exact retrieval an agent runs.

tool:query_applicable_rules

Lessons graduate into rules

Rules can only be derived from lessons in the enforced state, and a lesson cannot be enforced without a documented prevention mechanism (checked by a database constraint). Status moves forward only; mis-filed records can be hard-deleted by an authorised credential.

constraint:CK_build_lessons_enforced_requires_prevention

Usage per session

Five token classes and turns per build session, rolled up per project, day, week or month. Token counts, not dollars. Idempotent per transcript. One Claude Code SessionEnd hook, or any client calling the tool.

tool:record_build_session_usage

Flow metrics

Cumulative flow, throughput, aging work in progress and cycle time — dashboard only, read straight from the backlog, and honest about having no transition history.

page:/flow

Markdown import

Import existing markdown ledgers: preview first, then commit; entries already imported are skipped by a fingerprint of their source block, so re-running is safe.

tool:import_session_state_from_markdown

Lesson candidates are proposed by an optional LLM detection pass over your resolved backlog (bring your own Anthropic key, off by default); they cannot be typed in by hand, and each candidate is promoted, rejected or superseded once — terminal states are absorbing. Every session-state lifecycle event — sessions started and ended, decisions appended and corrected, backlog filed, triaged, assigned and reprioritised, lessons and rules created and changed — is written into the same tamper-evident SHA-256 audit chain as messaging: 38 of 123 event types. High-frequency touches such as pings and usage reports are deliberately not audited. A workspace can browse its own chain in the dashboard and read it over REST and MCP, with the operator-significant families withheld; the mechanism is on the security page.

API surface

One capability, three surfaces

The MCP tool, the versioned REST route under /v1 and the dashboard page — all three run the same service layer, and for every session-state aggregate REST and MCP serialize through one shared serializer and are tested byte-identical.

start_build_session
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "start_build_session",
    "arguments": {
      "project_id": "01K…",
      "computer": "my-laptop",
      "branch": "feat/lease-sweep",
      "intent": "make the expiry sweep lose to a live lease"
    }
  }
}
REST
POST /v1/build-sessions
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "project_id": "01K…",
  "computer": "my-laptop",
  "branch": "feat/lease-sweep",
  "intent": "make the expiry sweep lose to a live lease"
}
Dashboard
app.postmq.com/build-sessions          active sessions first, then closed
app.postmq.com/build-sessions/{id}     current state, lead, history entry, usage
query_applicable_rules
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "query_applicable_rules",
    "arguments": {
      "project_id": "01K…",
      "file_paths": ["src/PostMQ.Infrastructure/Accounts/CredentialIssueService.cs"],
      "operations": ["modify"],
      "languages": ["csharp"],
      "project_attributes": ["sql-locking"],
      "task_context": "take the account row lock before inserting a credential"
    }
  }
}
REST
POST /v1/rules/applicable
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "project_id": "01K…",
  "file_paths": ["src/PostMQ.Infrastructure/Accounts/CredentialIssueService.cs"],
  "operations": ["modify"],
  "languages": ["csharp"],
  "project_attributes": ["sql-locking"],
  "task_context": "take the account row lock before inserting a credential"
}
Dashboard
app.postmq.com/applicable-rules        the same retrieval, replayed by a person
app.postmq.com/rules/{id}              one rule: triggers, retrieval_count, last_retrieved_at
append_decision_log
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "append_decision_log",
    "arguments": {
      "title": "Ack and sweep predicates live in one UPDATE",
      "body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
      "entry_date": "2026-08-18",
      "has_public_contract_impact": false
    }
  }
}
REST
POST /v1/decision-log
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "title": "Ack and sweep predicates live in one UPDATE",
  "body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
  "entry_date": "2026-08-18",
  "has_public_contract_impact": false
}
Dashboard
app.postmq.com/decision-log            newest first; filter by project; search title and rationale
app.postmq.com/decision-log/{id}       one entry, its rationale, what it corrects, its session
record_build_session_usage
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "record_build_session_usage",
    "arguments": {
      "build_session_id": "01K…",
      "client_session_id": "8f3c…",
      "input_tokens": 41200,
      "cache_write_5m_tokens": 12800,
      "cache_write_1h_tokens": 0,
      "cache_read_tokens": 1810400,
      "output_tokens": 22600,
      "turns": 118,
      "sidechain_turns": 9,
      "agent": "claude-code"
    }
  }
}
REST
POST /v1/usage
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "build_session_id": "01K…",
  "client_session_id": "8f3c…",
  "input_tokens": 41200,
  "cache_write_5m_tokens": 12800,
  "cache_write_1h_tokens": 0,
  "cache_read_tokens": 1810400,
  "output_tokens": 22600,
  "turns": 118,
  "sidechain_turns": 9,
  "agent": "claude-code"
}
Dashboard
app.postmq.com/usage                   tokens and turns per project, day, week or month
app.postmq.com/flow                    cumulative flow, throughput, aging WIP, cycle time
Guarantees and limits

What holds, how, and what is deliberately not built

A mechanism per row; the badge says whether it ships on, ships off, or is roadmap.

Session-state guarantees and limits: what holds, the mechanism, and its scope
what holdshowscope
Session start is idempotentC06 UQ_build_sessions_active — a filtered unique index on (project, computer, branch, actor); 201 on create, 200 on resume; concurrent starts cannot mint two active sessions shipped
Decisions cannot be edited or deletedC07 tg_decision_log_entries_append_only rejects DELETE and every UPDATE except the two source links; a correction is a new entry with corrects_entry_id shipped
Backlog terminal states are absorbingC14 tg_backlog_items_terminal_absorbing; resolve requires a closing PR, commit or closing notes; dismiss requires a reason shipped
A rule matches on every populated dimensionC09 file globs · operations · code patterns · languages · project attributes, AND across the populated ones; retrieval_count and last_retrieved_at bumped on each hit shipped
Rules derive only from enforced lessonsC12rules are advisory to the agent — enforcement stays in your CI derive_rule refuses a lesson that is not enforced; CK_build_lessons_enforced_requires_prevention refuses enforced without a prevention mechanism; status triggers move forward only shipped
Every lifecycle event is on the audit chainC19pings, usage reports and retrieval-count bumps are deliberately not audited 38 of 123 event types, written into the workspace’s SHA-256 chain in the same transaction as the row they record shipped
Usage is idempotent per transcript, in tokensC17 UQ_build_session_usages_report on (build session, transcript); five token classes + turns; token counts, not dollars; self-reported by the agent or the hook shipped
REST and MCP return the same bytesC05 one serializer per session-state aggregate, pinned by nine byte-parity test classes shipped
Search is a LIKE matchC62 query_decisions, query_backlog and search_build_sessions match an escaped substring with SQL LIKE, newest first, capped at 50; query_lessons matches the same way but comes back in lifecycle order, least-advanced first — not full-text search shipped
Paging style differs by surfaceC100 list_decision_log and list_build_sessions are keyset-paged over an opaque signed cursor; list_backlog_items and query_lessons are offset-paged with a total; query_decisions, query_backlog and search_build_sessions take neither cursor nor offset — they return a capped top-N shipped
Flow metrics are dashboard-onlyC18 /flow reads the backlog as it stands: no transition history, so aging is since creation, dismissed items are excluded, and there is no REST or MCP surface shipped
LLM assist is optional and off by defaultC13 lesson-candidate detection and rule enrichment run only when your own Anthropic key is configured; they propose, and a person promotes default-off
Import dedups by fingerprint over the raw blockC20 SHA-256 of each entry’s source block; preview first (dry run), commit once; an edited entry re-imports as new shipped
Bring your history

Import the ledgers you already keep in markdown

A decision log, a backlog, build-session history — import_session_state_from_markdown parses the corpus (up to 1 MiB per call; the REST multipart upload takes larger files), previews it as a dry run, and commits it on the second call. Entries already imported are skipped by a fingerprint of their raw source block, so re-running is safe, and every run is an append-only record with parsed, committed, skipped and errored tallies.

Three honest lines. The fingerprint is over the raw block, so an edited entry re-imports as new rather than updating the old one. The parser is the SpecStep markdown convention (SpecStepConvention.v1); another shape needs a parser first. Dry runs are not audited — only a commit lands on the audit chain. Setup is on the docs page; the import tools are in the catalogue.

We run it on this repo

The numbers are the project's own ledger

528 backlog items · 86 open · 438 resolved · 4 dismissed
938 pull requests merged
914of 944 commits on main carrying a session trailer

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

We run PostMQ on PostMQ; every number here is generated from the project's own ledger at build — never typed — and the dogfood check fails the build when the file goes stale. The commit trailer is what ties a commit to the session that produced it.

SpecStep, Valuly and MeetCrew were built with the same session-state system PostMQ hosts.

Start a session. The next one resumes it.

Create a workspace, connect your agent, call start_build_session.