Patch release. Terminal ticket files now agree with the ticket index, two operator-facing messages match the views they describe, and the package carries the metadata needed for the official MCP Registry.
Canceled and terminated tickets now leave the open queue. The ticket
index already marked these states as terminal, but their Markdown files stayed
under tickets/, while approved tickets moved to tickets/closed/. New
cancel and terminate operations now write the terminal record under
tickets/closed/, remove the open-queue copy, and carry any submission file
out of tickets/review/. The tool response reports the filed path. This is
forward-only: files left behind by earlier releases are not moved
automatically.
The dashboard Tickets badge now counts the rows in the view it opens. It previously showed only open tickets while the page listed the complete ticket history. Open tickets still control the badge’s hot styling.
An empty compaction candidate set is reported accurately. The measurement
helper now says EMPTY COLD SET and lists all exemption reasons: pinned,
inside AGENT_MEM_HOT_HOURS, or within AGENT_MEM_MAX_HOT. It no longer
implies that every entry is pinned or high-priority.
CI now runs all offline regression suites. The hot/cold ranking, handoff pinning, and board-lock tests were present at the repository root but missing from the enumerated CI command.
Clean installs stay on the compatible MCP SDK major version. The package
now declares mcp[cli]>=1.0.0,<2; MCP SDK 2.x removed the
mcp.server.fastmcp import used by this release. Existing 1.x environments
are unchanged.
server.json for the official MCP
Registry. The manifest identifies the existing PyPI package and stdio
transport, and exposes AGENT_PROJECT_DIR as a required non-secret filepath.
It does not change the MCP protocol, tool behavior, installation model, or
runtime command.Patch release. Two fixes to the hot/cold memory tiering, both found the same way: by watching the board throw away entries it had just been told to keep.
The hot-set overflow ranked on priority before recency, so new entries lost
their slots to old ones. _split_hot_cold decides is_recent for every
entry, then — if more entries than AGENT_MEM_MAX_HOT survive that pass —
re-sorts by (priority, timestamp) and truncates. Priority dominates, so the
recency decision made ten lines earlier is discarded, and a brand-new
unpinned priority: 0 entry sorts below every week-old priority: 1 handoff
on the board.
This is why compaction ate the digest written for it. memory_prepare_compaction
tells the agent to summarise the cold set with
memory_write(memory_type='context'); that digest lands unpinned at priority
0, the overflow step drops it straight into COLD, and the memory_compact
call it was written for archives it along with the entries it summarised.
Reproduced twice on a live board by two different agents. Nothing was lost —
memory_search_archive still finds them — but the next agent reads hot
first, so a digest in the archive is a digest nobody reads.
Overflow now ranks (pinned, in-hot-window, priority, timestamp). Pinned
still never loses a slot, entries inside AGENT_MEM_HOT_HOURS outrank stale
high-priority handoffs, and priority still orders within a tier. On a live
53-entry board the demoted set went from [oldest handoff, second-newest
entry] to the three oldest handoffs.
Every handoff ever written reserved a hot slot permanently. Both handoff
writers hardcoded priority: 3, pinned: True. The live board carried 33
handoffs, 17 of them pinned, against a 50-slot budget — and with pinned now
ranking first in overflow, that reservation is absolute rather than merely
likely.
pinned on a handoff never meant “show me”: _latest_handoff_lines, the
briefing’s LATEST HANDOFF block and the onboard last-handoff line all select
by memory_type and recency, and both pinned lists explicitly exclude
handoffs. It only ever meant “do not compact me”.
memory_handoff still pins, but now demotes the author’s earlier handoffs
first, so the board keeps one pinned handoff per agent. The auto-handoff
written by memory_submit_ticket is now priority: 1 and unpinned — it is a
routing notice that expires when the review lands, and 21 of the board’s 33
handoffs were auto-generated. Forward-only, no backfill, keyed on
agent_name + memory_type: same rule and same shape as
_demote_rejection_warnings. Handoffs already on a board stay pinned until
their author writes another one, or memory_unpin clears them.
Patch release. One concurrency fix and one hygiene fix, both found by running the board rather than by reading it.
memory_onboard wrote agents.json without the board lock. It and
memory_agent_join both call the same helper, _join_agent_session, which
read-modify-writes the agent record. Only agent_join was decorated with
@_with_board_lock, so the unlocked path was the one every session opens
first. Two doors onto one write, and the open one was the front door.
_board_lock() is now an async context manager and _with_board_lock is
written in terms of it; the seven existing decorated sites are unchanged.
memory_onboard holds the lock only across _join_agent_session, not across
the briefing render, so a slow briefing cannot block the board.
memory_write borrows the lock with a comment marking that arrangement
temporary.
test_board_lock.py encodes the invariants rather than the symptom: every
mutating tool holds the lock, onboard must not hold it across the render, and
_join_agent_session calls no locked tool — nesting would deadlock
permanently, because flock is per open file description and _board_lock
opens a fresh descriptor. A six-process concurrent-write test runs for real.
Test fixtures carried a real username and two real ticket ids. The
fixtures needed realistic-looking values because the property under test is
that the compressor preserves paths and IDs verbatim, and real ones were used
instead of invented ones. Since pyproject.toml packages ["."], they
shipped inside the wheel. Now /Users/example/... and TK-000000000000.
Patch release. Everything here is polish and correction on top of v4.0.0, found by actually running v4 between two Claude Desktop instances and a Codex agent rather than by reading it.
Note on the number: memory_unpin is a new tool and retracts= /
idle_budget_min are new fields, which a strict reading of semver would call a
minor. Shipped as a patch deliberately, because v4.0.0 had no production
consumers yet and all of it is repair of that release.
Idle budget with STAND-DOWN (#10). memory_wait_for_event takes
idle_budget_min (minutes, default 15; 0 listens indefinitely) and the
server counts consecutive empty parks in the agent’s watch cursor. When the
budget is spent it answers STAND-DOWN instead of another idle, so an
unattended listener stops on its own rather than looking wedged to whoever is
watching. Every idle reply prints its countdown: idle 3/5 — ~6 min to
stand-down.
Budgets are stated in minutes because a human watching a silent loop is
counting wall clock, not iterations — a compliant agent looping for 20
minutes looks stuck even when it is exactly on budget. STAND_DOWN is a
status distinct from IDLE, so a loop matching on idle to decide whether
to re-arm cannot read a stand-down as permission to continue. The counter
advances only on a completed empty park and resets only on a real event —
never on re-arming, or a tight re-arm loop would reset its own patience and
never stand down.
The listen prompt previously said “Keep going; there is no round budget”,
which would have talked an agent straight past its own stand-down; it now
defers to the server.
No migration needed — cursor files written before this carry no
idle_count and read as zero.
memory_unpin (#16) — demotes a pinned memory without deleting it.
priority=3 means never compact this, not important: an auto-pinned entry
is exempt from compaction forever and competes for the 5 onboarding slots, and
until now there was no way to undo one. A board accumulated pinned rejection
warnings for tickets that had long since closed, and a warning later found to
be wrong stayed pinned at critical, so every joining agent read a bug that
did not exist as the first thing on the board. Clears pinned and
pinned_summary, drops priority to 1, records unpinned_by / unpinned_at /
unpin_reason. The entry stays in memories.json.
retracts= on memory_write (#16) — a correction demotes what it
corrects, in one call. The target gains a [RETRACTED] prefix and a
retracted_by back-link; the new entry gets a retracts forward-link. A
bogus target id is refused before the new entry is appended, so a failed
retraction leaves nothing behind.
Rejection warnings auto-demote on terminal states (#16) — retitled
[RESOLVED:closed|canceled|terminated] once their ticket resolves, wired at
all three sites. Discrimination is by the auto-rejection tag plus
related_tickets, never by title string, so a human’s hand-written warning
about the same ticket is left alone. Forward-only: existing pinned rejection
warnings are not backfilled — clear them with memory_unpin.
The server declares its own identity (#13) — website_url, plus an icon
as a data: URI when docs/assets/on-board-icon.png exists. A client that
cannot resolve a server’s identity substitutes whatever it has cached: Codex
rendered On Board under the name and logo of an unrelated shopping connector.
No placeholder branding is invented; with no file present, nothing is
declared.
thrift_compress.py + tools/measure_compaction.py (#11) — token-thrift’s
text transform, vendored stdlib-only (no new runtime dependency), behind
AGENT_MEM_THRIFT_COMPACT which defaults OFF. Compaction digests are not
compressed until it is switched on.
Measured, not estimated: 4.9% over 17 real board units (o200k_base,
fidelity gate 17/17). Far below the −17.8% simulation, and that is the
fidelity fix working rather than the compressor underperforming — entry titles
are now kept verbatim, so only decision bodies and warning lines remain
compressible. A smaller honest number replaced a larger unsafe one.
The harness carries --self-test, which asserts the fidelity gate goes red
on a rewritten title, a rewritten heading, a dropped ticket id and an altered
number. It exists because the original fidelity() scored 1.0 on live title
corruption: a gate that cannot fail on its own requirement is decorative.
CI actually runs the offline suites. The step was pytest tests, and the
offline suites live at the repo root, so the role gate (18 tests) and the wait
primitive (29 tests) — the two things v4 is about — had never run in CI. Now
enumerated explicitly; test_a2a_live.py and test_a2a_multiprocess.py stay
out because they need a live board. 116 tests now run.
Submitting no longer hands off an agent that owes a review (#13). If the
submitter owns another ticket already sitting in submitted, leaving strands
it — observed live: an agent submitted its own ticket, auto-handed off, and
its peer’s submission sat unreviewed with nobody on board to adjudicate. The
reply now names the ticket that is owed. Narrow by design: with nothing owed,
the previous behaviour stands.
The self-review denial no longer tells a solo owner to ask itself (#13).
The message reports created_by, which was always correct — but on a solo
create → claim → do cycle that is the same agent, so a correct lookup
produced a useless sentence. It now names the role in that case.
Ticket .md files no longer carry TicketStatus.SUBMITTED forever (#13).
Two bugs in one line: f-string formatting of a str-mixin Enum yields the
repr on Python 3.11+, and the file was written at submit and never rewritten,
so closed tickets still read submitted. _index.json remains the source of
truth, but a human diagnosing a problem reads the .md — a stale one is how a
reviewer here concluded a rejection had been silently reverted when the ticket
had simply been re-submitted.
_board_snapshot re-parsed
memories.json and the ticket index on every 2 s poll tick, per parked
listener. It now keys on (st_mtime_ns, st_size) of both source files and
reuses the cached snapshot while neither has moved — the common case while
parked. 25 ticks cost one parse instead of 25. The cached dict is returned by
identity under a documented read-only contract.Major. Until now the board was pull-only: an agent learned that a peer created a ticket or left a handoff only when a human told it to look. v4 adds a blocking wait primitive so agents wake each other through the board — verified live between two Claude Desktop instances and one Codex (GPT) agent, including a full reject → fix → resubmit cycle closed with zero human relay and a reviewer-reproduced sha256.
ticket_roles.py).
Submitting requires having claimed the ticket; reviewing/closing requires
being the ticket’s creator or holding a main/lead/reviewer role, and
is denied to whoever executed it. Solo workflows (create → claim →
review your own ticket) now require allow_self_review=True on
memory_review_ticket, which permanently stamps the ticket
SELF-REVIEWED — no independent check..agent-mem/ boards)watch.json is read
once per agent, then superseded by per-agent watch-<agent>.json
(regression-tested: no replay storm on upgrade).created_by == claimed_by), add
allow_self_review=True at review time or onboard a second identity as
reviewer.fcntl is unavailable; the board lock degrades to pre-v4
last-write-wins semantics instead of failing to start. Single-instance
Windows use is unaffected; multi-instance Windows boards keep the old
concurrency risk..agent-mem/: watch-<agent>.json, .board.lock,
and per-process *.tmp.<pid> during saves — all inside the already
gitignored directory. Entry points, tool names and existing tool
signatures are unchanged; new parameters (stay_active,
allow_self_review) default to previous behaviour.listen prompt, field
descriptions, and the compaction workflow — to be written in compressed
English with code/paths/IDs verbatim; human-skim fields (titles, pinned
summaries) stay readable in any language. Rationale: Thai measures 2.96×
English tokens for identical content, and on a board one hop’s output is
the next hop’s input, so the language rule compounds across every wake.memory_wait_for_event — park in one tool call until a peer creates a
ticket, changes a status, or assigns work. Checks before blocking (a re-arm
after a gap drains its backlog in 0s), one wake returns the whole queue,
and an agent never wakes on its own actions (attribution read from
per-transition stamps: claimed_by, submitted_by, reviewed_by, …).listen MCP prompt — the cheap re-arm loop, with measured guidance:
Claude Desktop cancels at ~240s per call, not per turn, so keep
timeout_s ≤ 180 and re-arm freely; stdio clients may pass long_wait.ticket_roles.py) — completed ≠ success: the
executor’s terminal move is submitted; only the owner or a
main/lead/reviewer closes. Holds even when owner == executor;
allow_self_review=True is the explicit escape and stamps the ticket
SELF-REVIEWED. Assigned tickets are not claimable by others.stay_active on memory_submit_ticket — listeners stay on board to catch
the verdict and take the rejected → retry path.review_notes / fix_instructions are stamped on
the ticket and carried on the wake event, so a rejected worker knows why
without a human relaying it. Rejections render as REJECTED → open via the
durable rejection_count delta (the transient rejected state is never
observable to a poller).tickets/_index.json mutations (and
memory_agent_join) hold an advisory flock, so simultaneous create/claim
from separate server processes no longer lose writes.JsonMemoryStore.save — concurrent saves from two
instances previously interleaved into one fixed .tmp path and corrupted it
(reproduced by the new multiprocess stress test).watch-<agent>.json) — single writer per file.Current GitHub Release collecting all major changes after v3.5.2, plus a security patch for the open Dependabot MCP Python SDK alerts.
mcp dependency from 1.27.1 to 1.28.1.index.html GitHub Pages landing page.Update this On Board checkout only:
bash update.sh
bash doctor.sh --self
Update this checkout and refresh every registered linked project:
bash update.sh --refresh-linked
bash doctor.sh --all-linked
--refresh-linked also performs the normal update first and preserves each
project’s registered hook mode.
If upgrading from v3.5.2 or older, rerun setup-project.sh /path/to/your/project
and doctor.sh /path/to/your/project for projects that are not in the linked
project registry.
Restart your MCP clients after updating.
Agent startup is now shorter, with full context moved back to explicit tools.
memory_onboard now returns compact current context instead of embedding full memory_get_briefing and full memory_list_tickets output.memory_get_briefing, memory_list_tickets, memory_read, memory_search, memory_links, memory_doctor, memory_status, and memory_token_usage.memory_write(priority=3) now auto-pins critical memory and stores a compact pinned_summary while preserving raw content.pinned_summary.memory_pin public tool. Use memory_write(priority=3, pinned_summary=...) for critical pinned memory.bash update.sh
bash update.sh --refresh-linked
Restart your MCP clients after updating.
One central On Board checkout can now safely serve and refresh multiple linked projects.
.onboard/linked-projects.jsonsetup-project.sh --list-linked and setup-project.sh --all-linkeddoctor.sh --list-linked and doctor.sh --all-linkedupdate.sh --list-linked and update.sh --refresh-linkedAGENT_SETUP.md as an agent-facing setup guidedoctor.sh as a read-only setup checkersetup-project.sh now generates .onboard/ project config, AGENT_CONTROL.md, and a project dashboard launcherupdate.sh now skips unrelated folders, backs up overwritten files, and requires an explicit refresh for linked projectspython3 onboard_server.py, which normally runs .venv/bin/python server.py and rebuilds .venv only if missingbash update.sh
bash update.sh --refresh-linked
Restart your MCP clients after updating.
Small template fix for projects that use external reference directories.
AGENT_MEM_CONTEXT_DIRS to the uv run --directory MCP config templatememory_context_dirs / memory_context_readA small install-path release for running On Board from a source checkout with uv.
uv sync / uv run --directory install and MCP configuration docs for source checkoutsconfigs/uv-mcp.json template for MCP clients that can launch through uvuv.lock for reproducible local uv sync installsA tighter agent workflow release: one-call onboarding, XML protocol hints, ticket-linked memory, data-health checks, and dashboard linkage.
memory_onboard as the primary one-call agent entrypoint: join session, read briefing, inspect tickets, and surface data-health warningsmemory_get_briefing(ticket_id=...) for ticket-scoped onboarding with ticket details and related memoriesmemory_links for viewing ticket, memory, file, agent, and tag linkagememory_doctor data-integrity warnings for duplicate active agent identities, orphaned claimed tickets, invalid ticket schemas, and duplicate memory IDsSKILL.md now matches the significant-action memory protocol and current ticket schemamemory_write now accepts related_tickets and skips exact recent duplicatesImproved the daily agent workflow: cleaner briefing modes, setup diagnosis, ranked search, and opt-in local vector-style search.
memory_get_briefing(mode=...) with brief, normal, deep, and handoff-onlymemory_doctor for read-only checks of hooks, rules, runtime memory, and ignored pathsmemory_search_vector with opt-in local backend (AGENT_MEM_VECTOR_BACKEND=local)memory_search and archive search now rank by relevance, priority, pinned state, and recency.agent-mem/*.json remains the source of truth; vector-style search is a cache/search aid onlybash update.sh
# Restart Claude Desktop / Cursor / Codex
Fix: memory_agent_join no longer creates duplicate entries when the same agent rejoins.
memory_agent_join — same name + same platform → update existing active entry instead of KIA + create new
agent_role and task_focus refreshed on rejoinjoined_at preserved (session continuity)🔄 Rejoined instead of 🟢 On Board_mark_prev_kia — removed rejoin_same_identity KIA case; same-identity rejoins now handled by update logicAgents joining multiple times per session (crash/restart/reconnect) accumulated duplicate entries in agents.json under the same name. Dashboard and briefing showed the same agent multiple times. Reported by claude-opus4.7-web-20apr26 after multiple same-session joins.
bash update.sh
# Restart Claude Desktop / Cursor / Codex
Existing agents.json works as-is — no migration needed.
Tightened memory_create_ticket to force structured ticket specs — prevents missing-deliverable rejections.
memory_create_ticket — 3 new required fields: target_url, scope, required_fields
target_url (str) — URL the executor navigates to; MCP rejects if missingscope (enum) — must be READ-ONLY, interactive-no-send, or interactive; MCP rejects invalid valuesrequired_fields (list, min 1 item) — deliverables executor MUST capture; MCP rejects empty listmemory_create_ticket — 2 new optional fields: forbidden (default []), selector_hints (default []).md file for Jonhny to relay to subagents verbatimSimulation (2026-04-21) showed 28.6% rejection rate from Opus tickets missing required_fields in ticket bodies. Fixing this at the MCP layer means bad tickets are rejected at creation time, not after a subagent wastes a full run.
bash update.sh
# Restart Claude Desktop / Cursor / Codex
Existing tickets are unaffected — new fields only apply to newly created tickets.
Improved agent lifecycle tracking and dashboard UX.
last_activity updated on every tool call (10 tools)agent_name, shows session countAGENT_MEM_IDLE_KIA_MIN env var (default: 30 minutes)--poll-sec dashboard flag aliasbash update.sh
# Restart Claude Desktop / Cursor / Codex
No data migration needed — existing agents.json works as-is.