Open work
Aggregate of deferred items across retros plus standing risks — the “if I had two hours” backlog. For exhaustive partials and Slack parity, see Remaining Work.
Updated at each cluster retro. Baseline: code on main at v314.0.0 (Product Ladder 102+ complete at v120 / maidan-scale-1.0; post-gate hardening 121+; MCP 2026-07-28 300–303, mail 304–306, Slack/GitHub projectors 307–312, SDKs 294–299, launch-prep 313–314). Reconciled against code at v126 (Cluster 127), v143 (Cluster 144), v273 (Cluster 273), and again at v314 (2026-08-28 4-thread research sweep — see "Pre-launch fixes + flagship arc" below).
Post-flagship audit program (2026-08-30 full-repo audit — CURRENT)
A 9-agent full-repo audit (code / deferred / docs / product / perf / security / testing /
architecture → synthesis; journal wf_23c0c888-03f) ran after the flagship arc closed at
v331. Verdict: ~90% of "code perfect / docs immaculate / no major gaps / production-ready /
compelling." Engineering discipline is top-decile (no lib unwrap/TODO; REST transactional
outbox; comprehensive app-layer RBAC; LSN causal replica routing validated vs real replication;
an honest self-correcting backlog). The single dominant theme: the MCP transport was never
brought to parity with REST — and MCP is the product's primary agent interface, so the
deficient transport is the one agents actually use. The two sharpest items were code-verified
by the maintainer (file:line below), not taken on faith. Run as the next program (normal
cluster cadence: retro + vX.0.0 tag each).
P0 — fix before promoting:
- P0.1 — ✅ FIXED (Cluster 332). MCP artifact tools now enforce Cluster-204 tenant isolation:
get_artifact_metadata+ themaidan://artifacts/{sha}resource read gate onartifact_ref_exists(auth.workspace_id, sha)→NotFoundwhen absent (no cross-tenant oracle, matching REST); MCP uploads (single-shot + multipart complete) record the per-workspace ref viarecord_artifact_ref;resources::readusesmeta.size_bytesinstead of loading the blob. e2emcp_artifact_tools_enforce_tenant_isolation(workspace B denied on both the tool + resource paths; A allowed). Was: MCP artifact tools bypassed Cluster-204 (cross-tenant leak).crates/maidan-mcp/src/tools/artifact.rs::get_artifact_metadatatakes noauthand callsstore.get_artifact_by_shawith noartifact_ref_existscheck;crates/maidan-mcp/src/resources.rs:57-60(maidan://artifacts/{sha}read) does the same and returns the full blob bytes — so anyworkspace:readbearer reads any tenant's artifact bytes+metadata by SHA (REST returns 404 viaensure_artifact_ref).upload_artifactalso usesupsert_artifact(no ref → REST 404). Fix: threadauthin, gate reads onartifact_ref_exists(auth.workspace_id, sha), upload viaupsert_artifact_with_event+ref_workspace(the Cluster-330snapshot.rstool is the template); drop the full-blob read inresources.rs, usemeta.size_bytes. Add a cross-tenant e2e. Effort S.
P1 — high-value clusters (ordered):
- P1.1a — ✅ FIXED (Cluster 333): MCP
edit_messagenow usesedit_message_with_event+ the newMcpServer::publish_storedbus-notify, so an MCP edit appendsMessageEdited→ as-of replay sees the edit, the indexer reindexes, WS/SSE + notification router fire. P1.1b — ✅ FIXED (Cluster 334): the other 7 event-less write tools (cast_vote/add_reaction/remove_reaction/pin_message/unpin_message/record_mention/add_reference) now use*_with_event+publish_stored, and MCPpost_message/post_dm_messagepublishMentionRecordedper @mentioned member. P1.1 (MCP write-path parity) is COMPLETE — every MCP mutation emits its domain event like REST (WS/SSE, at-least-once, federation, notifications). - P1.1 MCP write-path parity: events + atomicity. ✅ VERIFIED (edit_message). The 8 event-less MCP
write tools (
cast_vote/add_reaction/remove_reaction/pin_message/unpin_message/edit_message/record_mention/add_reference) call plain non-*_with_eventstore methods and append no domain event;tools/message.rs::edit_messagecallsstore.edit_message(event-less) → an MCP edit appends noMessageEdited, so the flagship as-of replay returns the stale body forever and embeddings never reindex (stale semantic search); MCPpost_messagenever publishesMentionRecorded(no agent@mentionnotifications /wait_for_mention). Migrate to*_with_event+ a sharedMcpServerpublish; sequenceedit_messagefirst (sharpest correctness bug). Effort M. - P1.2 — ✅ mostly DONE (Cluster 335). The user-visible divergence is closed: the MCP context
assembler now uses batched shared helpers (no per-message N+1) and surfaces an
artifactsarray, matching REST; the sha extractor is shared viamaidan_types::artifact_shas_from_metadata. Deferred (maintainability-only): the full cross-crate assembler hoist intomaidan-router— blocked by aThreadContextname collision (router already exports a resolution struct of that name) + utoipa- feature propagation + afuturesdep, a multi-cluster refactor whose remaining payoff is only ending theas_ofdouble-impl (and the trickiest shared logic — the message fold — already goes throughmaidan_types::reconstruct_messages_through). Revisit if the two assemblers start to drift. - P1.3 — ✅ DONE across both transports (Clusters 336 + 337): MCP
whoamitool ({member_id, workspace_id, capabilities, is_bearer, bypass}from auth) +initialize.instructionscold-start guide +AuthContext::capabilities()(336); RESTGET /metwin ({member_id, workspace_id, capabilities, is_bearer}from auth,workspace:read, full new-route preflight) (337). An agent handed only a base URL + token can now self-discover itsmember_idover either transport. Optional arg-defaulting (author_id/member_id←auth.member_id) still deferred (touches many tools; self-discovery already unblocks the hero loop). - P1.3 (original) Agent cold-start:
whoami+ populatedinitializeinstructions. Nowhoamitool and no/meroute exist, yet every hero-loop tool needs the caller's ownmember_id; MCPinitializeomits the specinstructionsfield. Add awhoamitool +GET /me(member/workspace/capabilities), populateinitialize.instructionswith the 6-tool hero loop, optionally defaultauthor_id/member_idtoauth.member_id. Cheapest adoption unlock. Effort M. - P1.4 Post-path round-trip reduction. Split into two clusters.
- P1.4a — ✅ DONE (Cluster 338):
publish_routed_mentions(REST + MCP) no longer re-runsresolve_message_chainper post — it routes viaroute_mentions_in_messagewith the workspace the caller already resolved, and short-circuits when the body has no@handles(zero store work for a plain post). Removed the now-unusedroute_mentions_for_message. - P1.4b — ✅ DONE (Cluster 339):
maidan_auth::authorize_threadresolves the thread'sThreadScope {workspace_id, channel_id, thread_id}and authorizes the caller in one fetch;ensure_thread_accessdelegates to it (rule single-sourced; also sheds its own duplicateget_channel). ~30 handlers acrossmessage.rs/thread.rs/social.rs/skills.rsmigrated — those using the scope callauthorize_thread, the rest keepensure_thread_accessand drop the redundantresolve_thread_context+ensure_workspace. Behaviour-identical (404/403, same messages); thread+channel fetches halve on that surface. - P1.4c — ✅ DONE (Cluster 340):
maidan_auth::authorize_messageresolves theMessageScope {workspace_id, channel_id, thread_id, message_id}and authorizes in one pass (viaauthorize_thread);ensure_message_accessdelegates to it. ~12 handlers inmessage.rs/social.rsmigrated (edit/tombstone/purge/seed use the scope; votes/reactions/ get/edits/mentions keepensure_message_access). Message-scoped fetches drop ~5→3. Audit P1.4 is complete (338 + 339 + 340). Residual: the channel-keyedresolve_channel_contextsites (create/list threads) — 2 low-traffic handlers, left as-is.
- P1.4a — ✅ DONE (Cluster 338):
- P1.5 Egress wire-path tests + LSN replica CI (§3.1/§3.2).
- Egress wire tests — ✅ DONE (Cluster 347):
SlackWebClient/GithubApiClientgained awith_base_urlconstructor +egress_wire_e2edrives the real clients against a loopback recorder (exact URL/headers/body + success/error decoding). Optional follow-up: an SMTP wire test against an in-process catcher (the mail path already has a recording-mock e2e + connect-free config validation). - LSN-replica CI job — TODO: a CI job running
scripts/replica-harness.sh(two-Postgres streaming replication) that un-ignores the#[ignore]d LSN routing tests. Deferred as its own cluster — it needs a heavy two-Postgres Docker setup in CI; the routing is already validated locally against the harness. Effort M.
- Egress wire tests — ✅ DONE (Cluster 347):
P2 — polish (do after P0/P1). ✅ DONE (Cluster 341): A2A gRPC doc contradiction (reconciled
Architecture.md + Protocols.md to Claims.md's honest "partial" — gRPC is get/cancel/list only,
verified in a2a_grpc/mod.rs); tool-count drift (~78→85 in Framework Integrations.md +
examples/README.md + Adoption.md); README image pin v315→v339; Architecture.md
Capability-Map.md dead GitHub link → Capability-Map.md. Also ✅ DONE (Cluster 342): Integration.md now documents the flagship context surface
(as_of time-travel, glossary-in-pack, context snapshot, lean edits, seed/re-ask, tool-transcript)
in a new "Fidelity & context" subsection, with MCP-tool parity; folded a Cluster-341 miss
(Protocols.md "78" → 85 tools). ✅ DONE (Cluster 345): MCP post_message slash-dispatch —
user chose parity: a dependency-inverted maidan_mcp::SlashDispatcher (implemented by
maidan-server, attached to McpServer in main.rs) lets the MCP post path run registered slash
commands + merge the {slash_command, slash_response} metadata like REST; the MCP no-slash post was
also moved to the atomic outbox path. ✅ DONE (Cluster 346): projector link-management — the
Slack/GitHub projectors shipped ingress + egress + a store link table but no route created a link
(egress could never fire); added POST/GET/DELETE /workspaces/:wid/{slack,github}-links
(channel/workspace derived from authorize_thread; workspace-scoped unlink). MCP link tools are an
optional follow-up. ✅ DONE (Cluster 348): the notification fan-out mute check is now
batched — Store::filter_muted_members(kind, &[MemberId]) (SQLite dynamic IN, Postgres = ANY)
resolves the muted subset in one query; the fan-out writes only the unmuted (concurrently, per 344),
cutting 2 × followers toward followers + 1 round-trips. Remaining P2 (code-side): the
notification multi-row batch INSERT — the further optimization: collapse the writes too into one
INSERT … ON CONFLICT DO NOTHING RETURNING (Postgres UNNEST, SQLite chunked dynamic VALUES under
the 999-param limit; email side-effect keyed off the RETURNING set) → ~2 round-trips; Store
256-method god-trait split (large, low external value — recommend deferring); README no visual media
/ no paste-ready invite. ✅ DONE (Cluster 344): notification-router
O(followers) serial round-trips — the MessagePosted fan-out now runs per-recipient writes with
bounded concurrency (buffer_unordered, cap 8), de-serializing the head-of-line block.
✅ DONE (Cluster 343): list_threads unbounded
(last unpaginated list) — now keyset-paginated via page_threads_for_channel on the REST route + MCP
tool (default 100, clamp 1..=500); unbounded variant kept for internal full-list callers.
DECLINE / already-covered (not gaps — do not spend a cluster): the flagship optional tail (seed
pack/prefix, WorkSeeded, structure_only template) + Postgres RLS — explicitly DECLINED in
Decisions (## Product scope + ## Security); the legacy /inbox authz "defect" — verified FALSE
POSITIVE (bearer-only, sessions 401); outbox multi-replica double-publish (K8) — deferred with a correct
fix spec; Postgres benchmark numbers / coverage floor / context_query_count flake — real but P3.
Post-272 forward work (next program)
The optional-deferrals sweep (267–272) closed the last program. The next body of
work was scoped by the 2026-08-25 strategy pass — the detail and rationale live
in the strategy pack (Handoff.md is the index →
Pre-Public Hardening.md,
Path to Impressive.md,
Expansion Bets.md, Launch.md,
Protocols.md, Providers.md). This section is the
canonical backlog; the pack is the "why." Nothing here is committed to as a program
yet — pick the next arc with the maintainer, then run it through the normal cluster
workflow (retro + vX.0.0 tag each).
| Item | What / why | Detail |
|---|---|---|
✅ MCP 2026-07-28 upgrade (headline) — DONE (Clusters 300–303, tags v300–v303) | The 2026-07-28 revision (stateless Streamable HTTP; Mcp-Session-Id + initialize handshake gone; SEP-2243 Mcp-Method/Mcp-Name routing headers) is shipped: 300 additive negotiation (SUPPORTED = ["2026-07-28","2024-11-05"]), 301 stateless streamable core (a 2026 POST lands cold, no session; live-wait on GET /mcp/stream/WS), 302 SEP-2243 routing headers (present ⇒ must match body else 400), 303 advertise (default flipped to 2026-07-28; federation card/reference/Integration/Protocols updated; J2 retired). 2024-11-05 still accepted on explicit request. Deferred (niche, non-blocking): stateless server→client (request_client) + per-request _meta.io.modelcontextprotocol/clientInfo; ttlMs/cacheScope on list responses; optional server/discover. | Protocols.md (J1–J8); Handoff J3 / M.0 |
| ✅ Durable mail retry queue — DONE (arc 304–306) | Email delivery was best-effort, no retry. 304 maidan_mail_outbox table + store; 305 router enqueue_mail + a mail_worker (exp. backoff 30s→1h + dead-letter at 8 attempts, multi-replica-safe); 306 DLQ ops (GET /operator/mail/dead + POST …/requeue, token:admin). THE DURABLE-MAIL-RETRY ARC (304–306) IS COMPLETE. Follow-up (non-blocking): retention pruning of terminal outbox rows (the 186 sweeper doesn't cover maidan_mail_outbox). | Expansion Bets Bet 4 |
| ✅ MCP example pack + hero demo — DONE (Cluster 317, Bet 2 snippet pack) | Shipped the two-language lease demo (examples/lease_demo/ + scripts/lease-demo.sh: Python SDK + TS SDK workers both claim_next_thread on one channel → each gets a distinct task, drained queue → null; no LLM; verified end-to-end), Cursor/Claude MCP configs (/mcp/streamable, bearer, 2026-07-28), and rewrote the LangChain/AutoGen examples to filter to the six-tool hero loop (claim_next_thread/post_message/get_thread_context/set_thread_result/wait_for_result/wait_for_ready). FILTER ONLY — the 78-tool catalog is unchanged server-side and the pi 8-method seam stays callable; no seed_thread_from_message added. CI guards the new scripts/configs. | Expansion Bets Bet 2 |
| ✅ Thin client SDKs — DONE + PUBLISHED (arc 294–299) | TS/Python/Go/Rust clients under sdk/ at 0.1.0, LIVE on the registries (verified 2026-08-28: PyPI maidan 0.1.0, npm maidan 0.1.0, crates.io maidan 0.1.0, sdk-go-v0.1.0 tag; all four sdk-release runs succeeded 2026-08-27). Frozen v1 surface = docs/Client Contract.md; interop CI = report-only sdk-interop (299). Remaining (small): typed response models (0.2) + sdk/README.md still says "0.0.1 name-hold" (a lie — folded into the 316 honesty scrub). A second 0.1.0 upload is rejected. | Expansion Bets Bet 3 |
| Slack projector — IN PROGRESS (arc from 307, config-gated) | A projector (Slack Events ingress → Maidan thread → streamed egress, no LLM in Maidan) — a projector, not a product. 307 DONE: ingress foundation (POST /integrations/slack/events, signature-verified + url_verification; 404 when unconfigured). 308 DONE: maidan_slack_channel_links (slack channel → Maidan channel/thread/member) + store (link/get/list/unlink) + inbound routing (Slack message in a linked channel → Maidan thread; loop-prevention: skips bot/subtype + stamps metadata.slack). 309 DONE: egress — SlackSender/SlackWebClient (chat.postMessage) + route_message_to_slack (relays a linked-thread Maidan message to Slack, skips Slack-sourced messages via metadata.slack; hooked into the notification-router). THE BIDIRECTIONAL SLACK PROJECTOR (307–309) IS COMPLETE, config-gated + loop-safe. Follow-ups (non-blocking): link-management REST/MCP surface (store-level so far), a durable Slack egress outbox (best-effort today), a thread_id index on the links table. Live wiring needs David to create a Slack app + set MAIDAN_SLACK_SIGNING_SECRET/MAIDAN_SLACK_BOT_TOKEN. | Expansion Bets Bet 1 |
| Git / forge projector — COMPLETE (arc 310–312, config-gated) | GitHub App webhook → thread → issue/PR comment (GitLab/Gitea later). Explicitly not a Copilot clone. 310 DONE: ingress foundation (POST /integrations/github/events, X-Hub-Signature-256-verified + ping; 404 when unconfigured). 311 DONE: maidan_github_issue_links ((repo, issue)→Maidan channel/thread/member) + store (link/get/by-thread/list/unlink) + inbound issue_comment→thread routing (skips Bot comments + stamps metadata.github). 312 DONE: egress — GithubSender/GithubApiClient (REST POST /repos/{repo}/issues/{n}/comments) + route_message_to_github (relays a linked-thread Maidan message to a GitHub comment, skips GitHub-sourced messages via metadata.github; hooked into the notification-router beside the Slack egress). THE BIDIRECTIONAL GITHUB PROJECTOR (310–312) IS COMPLETE, config-gated + loop-safe. Follow-ups (non-blocking): the full GitHub App JWT/installation-token auto-exchange + Check Runs (a configured PAT/installation token works today), link-management REST/MCP surface (store-level so far), a durable egress outbox (best-effort today). Live wiring needs David to create a GitHub App + set MAIDAN_GITHUB_WEBHOOK_SECRET/MAIDAN_GITHUB_TOKEN. | Expansion Bets Bet 6 |
| Pre-public cleanup nits → superseded by Clusters 315 (correctness) + 316 (honesty scrub) | The real, verified nits (mail.rs "Not wired", outbox FOR UPDATE SKIP LOCKED, event_stream swallowed cursor, and the full doc-lie list) are now itemized under "Pre-launch fixes + flagship arc" below. This row is retired into those two clusters. | Pre-Public Hardening.md (A–K) |
| Provider recipes | Doc/compose recipes only (Ollama/TEI embeddings, R2/AWS-S3 next to MinIO, Keycloak + a SaaS OIDC, Neon/RDS/Supabase note, LibSQL/Turso feasibility). | Providers.md (I2–I6) |
| Public launch | Public-preview cut, un-hold, announce — gated on the maintainer's explicit go; keeps publish = false (no crates.io 1.0). | Launch.md (L1–L6) |
Pre-launch fixes + flagship arc (2026-08-28 research sweep)
A 4-thread research sweep (2026-08-28) audited the tree at v314 for anything more pressing
than the docs scrub, and researched the primitives that make Maidan exceptional at being the
room. Folded here. Lane tags: generic-room (any waiter, incl. pi), oss-adoption
(stars//play/registries), first-consumer (pi+soundcheck+bgv3 — lives in pi, not a maidan
cluster; pi SR-1). Sequence: correctness first (315) → honesty/no-clone (316) → snippet pack
(317) → token evidence (318) → the fidelity+context flagship arc. Full rationale: the strategy
doc docs/Undeniable Final.md + the four sweep reports (verdicts in-line here are the canonical
fold; that doc is the "why").
Cluster 315 — pre-launch correctness & security (generic-room)
Small, verified code fixes. Cleared as NOT findings (stale docs narration, verified fixed in code):
the subscribe_grants self-assertion, the DM generic-route participant gap, and single-tx
dual-write atomicity are all closed — see the standing-risks corrections below.
- legacy
/members/:id/mentions+/inboxself-only — ✅ DONE, but the "live defect" was a FALSE POSITIVE on verification. The audit flagged these (routes/member.rs) as missingensure_acting_member→ "a session can read another member's inbox." On verification the routes are mounted ONLY on the bearer-onlyprotectedrouter (auth::middleware, no session cookie accepted → a session gets401); there is no/ui/apimount (unlike the notification handlers, which Cluster 251 did session-mount — that's why their guard is load-bearing). The only callers are bearers, which are act-as-any by design (the 202/203 model). So there is no session-exploitable gap. Still added the threeensure_acting_memberguards as defensive consistency (strict no-op for current callers; pins a session to self IF these are ever/ui/api-mounted like 251). Test:legacy_inbox_and_mentions_are_bearer_only_not_session_reachable(documents the 401 reachability truth); guard logic unit-tested inensure_acting_member. hash-v1embedding default boots with no warning (main.rs:247-251) — "semantic search" silently returns near-random results ifMAIDAN_EMBEDDING_PROVIDERis unset.warn!at boot. (Repo's own K5.)event_streamreplay swallows the cursor advance (event_stream.rs:202-204,let _ =) — a failed advance is invisible (correctness is safe — a stuck cursor re-delivers, never skips; only observability suffers). Log/count it. (Repo's own K9.)- README "Run it (SQLite, no Docker)" 28-byte secret (
README.md:149,MAIDAN_SESSION_SECRET=dev-session-secret-change-me) won't boot —session/cookie.rs:18needs ≥32 bytes. Fix to a ≥32-byte value (the 314 headline one-liner was fixed; this sibling was missed). - Optional defense-in-depth (K3/K4):
AppState::subscribe_resume_secret()getterpanic!(state.rs:307) → boot invariant; gate theAUTH_DISABLED+ missing-secret test-secret fallback (main.rs:361-368) behind an explicitMAIDAN_ALLOW_INSECURE_RESUME_SECRET=1ack. (Deferred from 315 — behaviour-changing, low urgency.) - DEFERRED from 315 to its own cluster — outbox
list_pendingFOR UPDATE SKIP LOCKED(K8,postgres/outbox.rs:29-50): two relay replicas can both fetch + publish the same pending row before eithermark_published. Bounded (optimistic bus is at-most-once, consumers idempotent bylog_id). A naiveFOR UPDATE SKIP LOCKEDon the pooled SELECT is a no-op false fix — the lock releases when the statement's implicit tx ends, and the relay publishes + marks outside any tx. A correct fix needs either a lease column (migration + the mail_outbox/scheduler pattern) or wrapping the batch publish inside a held transaction (holds row locks across bus publishes — a robustness trade-off) + a multi-replica double-publish integration test. Its own small cluster, not a hasty 315 line.
Cluster 316 — honesty scrub + no-clone image (docs, oss-adoption) — ✅ DONE
- Docs honesty scrub — DONE. Corrected
Claims.mdA2A-gRPC overclaim (gRPC = task read/cancel/list only, noSendMessage);mail.rs"Not wired" comment (wired 249; unchecked Pre-Public-Hardening A6/K1);mcp/server.rsdefault-2024 comment (const is 2026);Framework Integrations.md2024→2026; two more won't-boot commands fixed —Pi.md'sdocker run -e AUTH_DISABLED=1(missing the ack → fail-closed; +:latest→:v315, + amaidan initauth-on path) andbook/src/introduction.md'scargo run(noMAIDAN_SESSION_SECRET, same class as the README headline);Threat-Model.mdseed →maidan init;sdk/README.md+Clients.md/Client Testing.mdbanners (0.1.0 published, not name-holds; MCP 2026);Promotion.mdstate banner (projectors/mail/SDK shipped, topics set, hero no longer cargo+AUTH_DISABLED); README "experimental A2A bridge"→"A2A v1.0 (JSON-RPC+REST; gRPC partial)";AGENTS.md/Integration.mdMCP-2024/A2A-subset;CLAUDE.mdlatest-tag v273→v315 + "Open Work is canonical";SECURITY.mdcosign example →<tag>. - No-clone image — smoke-gated, and the smoke reshaped it (KEY FINDING). The published
ghcr.io/david-engelmann/maidan-server:v315.0.0boots with auth on (verified:/healthok, multi-arch amd64+arm64, anonymously pullable), BUT it is distroless (no shell) and bundles onlymaidan-server, not themaidanCLI, andPOST /workspaces→401 — so the doc's planned "docker run …thenexec maidan init" is impossible (no CLI, no shell, no out-of-the-box token seed). Added an honest README "Prebuilt image (no clone)" note instead: images are signed- multi-arch, seed via
maidan initrun against your DB (release binary / one-shot job), verify cosign. Deferred to its own cluster: a true one-command no-clone eval (with the token flow bundled) needs the quickstart image (both binaries + a shell) published to GHCR — real infra, not a docs line.
- multi-arch, seed via
- Housekeeping — DONE:
v300.0.0GitHub Release was a stuck Draft → published (neighbors were all published). Nov311tag (311's code is inv312, commit6d3172c) — documented, tag NOT cut (correct). Quickstart image pin bump v312→v315 deferred (cosmetic). - Residual (fuller pass, low blast radius): the dense planning docs (
Clients.md,Client Testing.md,Promotion.md) still have inline 0.0.1/2024/AUTH_DISABLED references beyond the corrected top banners; the strategy pack (Handoff/Path/Expansion Bets/Launch/Adoption) stays a frozen 2026-08-25 snapshot (canonical = Open Work). Maintainer-facing; a full sweep is optional.
Cluster 318 — token-pack evidence (generic-room) — ✅ DONE
Shipped token_pack (crates/maidan-server/tests/token_pack.rs, the load_baseline pattern:
#[ignore]d harness + pure estimator unit-tested in CI) — the scoped context pack vs dumping the
whole channel = ~6.8× fewer tokens (in-process SQLite, 8 threads × 40 msgs; scoped ~4 951 vs
naive ~33 908 tokens), plus ~1.3× from lean edits. Bytes exact, ≈chars/4 tokens, ratio
tokenizer-independent. Benchmark.md "Context-pack token savings" section + Claims.md token row →
"Shipped + measured" with the evidence link — a ratio now exists behind the claim.
Follow-ups (optional): measure the MCP get_thread_context pack separately (it omits artifacts
vs REST); a maidan_context_tokens_total metric (not needed now — the doc exists). This closes the
launch-prep leg of the sweep (315–318); next is the fidelity + context flagship arc.
Fidelity + context flagship arc (generic-room — the differentiator)
✅ COMPLETE (Clusters 319–331, tags
v319.0.0–v331.0.0). Typed relations (319–320) → glossary store/REST-MCP/context-fold (321–323) → vote confidence (324) → agent conventions (325) → as-of context replay (326) → seed-from-message REST+MCP (327–328) → immutable context snapshot artifact REST+MCP (329–330) → arc closeout, optional tail declined (331). The optional tail (seedpack/prefixinclusion,WorkSeeded, flow template) is declined, composable from shipped primitives — see the ADR Decisions "Product scope". Items 1–7 below are the original plan, each annotated with its shipped/declined status.
From two converging research threads (pre-LLM annotation tooling + grounding/argumentation/provenance theory) plus the context/replay thread. This is the promotable category nobody ships: the room where agents build durable, checkable, replayable shared understanding — at a fraction of the tokens. It is one arc on one substrate (the typed reference edge). All rows are storage+API level — the server stores and serves typed edges, definitions, and immutable snapshots; agents interpret and re-run. Maidan stays a room, not a brain. Sequence measure-cheap-first (each a foundation for the next); zero-blast-radius foundations follow the 159/217/234 pattern.
- Typed reference relations (keystone) — ✅ DONE (Cluster 319).
Reference.relationis now a controlledRelationKind(supports / refutes / defines / depends / duplicates / grounds / supersedes+Other(String)escape so expressivity isn't lost), not a free string. Serializes as the bare snake_case string (wire byte-identical); both store backends bindas_str()/parsefrom_wire(column stays TEXT, no migration); RESTCreateReference+ MCPadd_referenceinputs typed;ReferenceAddedcarries it; OpenAPI/MCP schemas unchanged (string). The thread-DAG was already a special-cased typedblocks. Reverse-edge / by-type queries — ✅ DONE (Cluster 320):Store::list_references_to(reverse, reusesidx_references_dst, no migration) +GET /referencesreshaped to query src-or-dst + optionalrelationfilter + a new MCPlist_referencestool — "what refutes X / what references this" is now navigable. The "vocabulary registry" framing folds into the glossary (item 2);RelationKind::CONTROLLEDis the controlled set for relations. - Shared glossary / definitions layer — foundation ✅ DONE (Cluster 321). One flat
maidan_glossary_terms {id, workspace_id, term, definition, aliases, created_by, created_at, updated_at}table (pg0053/ sqlite0052,UNIQUE(workspace_id, term), aliases as JSONB / TEXT-JSON) +GlossaryTerm/NewGlossaryTermmodels +Store::{set,get,list,delete}_glossary_term(both backends;setupserts). Workspace-scoped (dropped the speculativechannel_id?). Kept flat — no hierarchy/broader-narrower (that's the KG-product line, a locked anti-goal). Thedefinesedge's target; the anti-drift pin. REST + MCP CRUD — ✅ DONE (Cluster 322):PUT/GET/DELETE /workspaces/:wid/glossary/:term+ list, and MCPset/get/list_glossary_term(s)(deleteREST-only). Context-pack fold — ✅ DONE (Cluster 323):GET /threads/:id/context+GET /workspaces/:wid/context(REST + MCP) carry aglossaryfield (include_glossary, defaulttrue, empty-omitted; workspace pack carries it once at the top, deduped). The glossary layer (321→322→323) is COMPLETE. - Optional
confidence— ✅ DONE forVote(Cluster 324): nullablemaidan_votes.confidence(pg0054/ sqlite0053),Vote/NewVoteOption<f64>(omitted when absent), RESTPOST/GET /messages/:id/votes+ MCPcast_vote, range0..=1at the API edge, re-cast upserts it. (ThreadResultalready stores arbitrary JSON, so aconfidencethere is a convention, not a schema change — folded into the conventions.) Conventions — ✅ DONE (Cluster 325): documented indocs/Integration.md"Agent conventions" with a convention-provingdecision_convention_e2eand zero new server code — a decision-record shape (kind/status/context/decision/consequences/ alternatives) overthread_results, supersession via thesupersedesreference edge +statusflip, and anackgrounding vote (version-pinned by time: stale once the message is edited after the ack'screated_at; optionalconfidence). Item 3 (confidence + conventions) is COMPLETE. - As-of context replay — ✅ DONE (Cluster 326).
GET /threads/:id/context?as_of=<event_id>- MCP
get_thread_contextas_ofarg reconstructs a thread as it stood at that event-log id, deterministic over the immutable log (no fresh search).Store::list_thread_events_through(both backends) + sharedmaidan_types::reconstruct_messages_throughfoldMessagePosted/MessageEdited(fullMessagepayloads) +MessageTombstoned→ the as-of message set with as-of bodies (a since-edited message shows its old body, a since-tombstoned message reappears); additive components cut by the anchor's time; glossary omitted; unknown id →404. Serves audit + re-ask-from-before-a-tangent. Deferred: workspace-context as-of (thread-scoped only in v1).
- MCP
- Seed-from-message gesture (the write side of "re-ask"). REST — ✅ DONE (Cluster 327):
POST /messages/{id}/seed({title, inclusion?, channel_id?}) spawns a titled, claimable child thread + aseeded_fromreference edge (new thread → source), source untouched, N per source, gatedworkspace:write+ source read + target-channel write. Inclusionpointer(default) +quoteshipped; lineage is theseeded_fromtyped edge (from #1), NOT a bespoke table — no new event kind (emitsThreadCreated+ReferenceAdded). NewRelationKind::SeededFrom. MCPseed_from_message— ✅ DONE (Cluster 328) (twin of the REST route; atomic*_with_event- bus-notify; 83 tools). Seed-from-message is COMPLETE over REST + MCP (pointer + quote).
pack/prefixinclusion +WorkSeeded— DECLINED (Cluster 331, Decisions "Product scope"): composable today as snapshot (#6) + seed-pointer + as-of replay (#4);WorkSeededis covered byThreadCreated+ReferenceAdded. Revisitable with demand.
- bus-notify; 83 tools). Seed-from-message is COMPLETE over REST + MCP (pointer + quote).
- Immutable context snapshot artifact — ✅ DONE (Cluster 329).
POST /threads/:id/context/snapshotfreezes the assembled pack (live oras_of) into the existing content-addressed artifact store (sha256 dedup, ref-guarded per 204); returns theArtifact(kind=context_snapshot), fetchable atGET /artifacts/:sha; gatedartifact:upload+ thread access. NewArtifactKind::ContextSnapshot+ migration pg0055/ sqlite0054widening the kindCHECK. Delivers tamper-evident "exactly what the agent was handed" + "prefix paid once, N angles". MCPsnapshot_thread_context— ✅ DONE (Cluster 330) (twin of the REST route; modernupsert_artifact_with_event+ Cluster-204 ref + bus-notify; 84 tools). Context snapshot is COMPLETE over REST + MCP. Remaining (optional convenience): the seedpackinclusion (attach a snapshot sha, ties #5↔#6 — composable today as snapshot + seed-pointer). - Flow / setup template — DECLINED (Cluster 331,
Decisions "Product scope"). Cloning a
setup (channels/skills/schedules/DAG skeleton) is covered by workspace export (187) + import-remap
(269–270): export, prune content, import. A dedicated
structure_onlyexport filter is the arc's highest-scope-creep item and "the room never scores which template is better" (a locked anti-goal); declined until a research round shows concrete demand.
Anti-goals (LOCKED — this is what keeps it "perfect at what it does, not more"): no span-labeling
UI, no inter-annotator-agreement metrics / adjudication queues, no Snorkel-style label model, no
coreference equivalence classes, no rich claim/argument graph (SciClaim), no bespoke decision-record
subsystem, no notes layer (a note is already a message + a reference edge), no KG hierarchy; no server
re-execution of models/tools, no branch-tree-with-merge, no A/B-eval over templates/contexts, no
prompt/version registry, no deep-copy fork, no non-deterministic "improved" replay, no shared-mutable
working set across forks. Not a harness. Not a labeling product. Not a reasoning engine. Not a
SaaS. The parked V-track (V1–V8 in docs/Undeniable.md §5), the V2 working-set-budget, /play,
hosted cloud, and the public launch stay gated on David.
Integration reality — projector/transport test coverage (generic-room)
From the 2026-08-29 mocks-vs-e2e audit (docs/Integration Reality.md, line-checked). The
in-process room e2es are real; the vendor-shaped HTTP paths are not exercised — the shipped
SlackWebClient/GithubApiClient/lettre SmtpTransport are never constructed in any test (only
the SlackSender/GithubSender/MailTransport trait mocks, which prove loop-prevention, not
the wire). Both egress clients also hardcode the vendor host (slack.com, api.github.com)
with no base-URL override, so they can't be aimed at a local sink. Fold as generic-room
test-confidence work (not next; the flagship arc leads):
- Real-client HTTP-path tests, no SaaS (Integration Reality §3.1). Add a base-URL override
to
SlackWebClient+GithubApiClient, then a second test that constructs the real client against a loopback axum sink — assert the JSON body, bearer/User-Agent/Accept, and the failure branches untested today (Slack's HTTP-200{"ok":false}; GitHub's non-2xx status). Copy thewebhooks_e2e.rsloopback-/hookshape; for SMTP, driveSmtpTransport::sendagainst Mailpit in Docker. The trait mocks stay for loop-prevention. Don't add a WireMock crate if an axum sink is simpler; never hit slack.com / api.github.com in PR CI. - LSN replica routing is claimed but CI-untested (§3.2). The read-your-writes contract
(
Maidan-Consistency-Token) has no running-server CI coverage — the harness tests (replication.rs/read_routing.rs/replica_routing.rs) are#[ignore]d andMAIDAN_DB_REPLICA_URLis unset inci.yml/compose.yaml. Fold: a compose primary+standby stand-in (or a job that runsscripts/replica-harness.sh+ un-ignores those tests). Distinguish the two env names — the harness readsMAIDAN_PRIMARY_URL/MAIDAN_REPLICA_URL; the server readsMAIDAN_DB_REPLICA_URL(whether the running server actually routes given that key is a separate, currently-untested check). - Honesty nits (§3.4, small docs/naming): the
*_e2e.rsfiles run in theintegrationjob, while the job namede2eis docker-compose smoke (Operations/Client Testing wording);slack_egress_e2e/github_egress_e2e/mail_worker_e2eoverclaim (trait mocks, not wire e2e);two_replica_*_e2eis two app sides on one Postgres (app-HA), notMAIDAN_DB_REPLICA_URLreplica routing;Client Testing.mdstill frames SDK CI ascompose --profile fullwhen the realsdk-interopjob is report-only SQLite +AUTH_DISABLED. - Live projectors = David's setup, NOT a maidan cluster (§3.3): a throwaway Slack workspace + app and a GitHub App on a throwaway repo behind a tunnel, one scripted round-trip each (loop-prevention the assert), nightly/manual. Do NOT start the live apps until §3.1 can aim the clients at a local host (else the first live run is also the first HTTP run).
- Anti-goals (§4): do NOT rebuild the room e2es (claim/MCP/auth/outbox are real); no Slack
Marketplace / Check Runs / Copilot; no real tokens in CI; do NOT graduate
sdk-interop/a2a-interopoff report-only here; do NOT bootcompose.quickstartin CI as the projector sink; compose federation-pull is not missing (federation-pull-smoke.shcovers it); not a harness.
Public-launch readiness (external review, 2026-08-25)
An independent agent review ran the released v272.0.0 binary and audited the repo
for public-launch readiness. Verdict: the core is strong (it independently praised the
self-healing NOTIFY floor, workspace-sharded fan-out, LSN causal replica routing, and
typed IDs — see the "code-backed talking points" below), and the blockers are
onboarding, honesty, and evidence — not missing features. Verified findings, folded
here as the canonical backlog:
| Pri | Item | Evidence / why | Notes |
|---|---|---|---|
| ✅ Done (276) | Runtime version was 0.0.0 | /health reported 0.0.0 because the release pipeline never set MAIDAN_VERSION (the version() override already existed). Fixed: the release build bakes the tag into the binary (native + cross via Cross.toml passthrough) and the image (Dockerfile ARG/ENV), with a build.rs rerun-if-env-changed so a warm cache can't ship a stale version. Cargo version = "0.0.0" intentionally stays (workspace is publish = false). Follow-up: an automated release-time assertion that binary/health/image-label/tag agree (currently self-proven by the release run) | |
| ✅ Done (277) | SQLite database is locked under write contention | Root-caused: SQLite is single-writer and sqlx's pool.begin() is deferred, so a multi-connection pool lets two writers each take a read snapshot and race to upgrade — a genuine deadlock busy_timeout can't resolve (a contention test showed a warm 8-connection pool failing ~90% of read-modify-write txs; 1 connection is clean). Fixed: the SQLite backend defaults to one connection (maidan_store::DEFAULT_SQLITE_MAX_CONNECTIONS, overridable via MAIDAN_DB_MAX_CONNECTIONS); Postgres unaffected. Guarded by sqlite_write_contention (default is clean under contention; an #[ignore]d probe documents the multi-connection deadlock). Follow-up: a read-pool + single-writer split (or BEGIN IMMEDIATE writes) would restore SQLite read concurrency without the deadlock, if it ever matters for the single-node backend | |
| ✅ Done (278) | One-command quickstart | docker compose up started only Postgres and the full profile built from source — no 5-minute path. Shipped compose.quickstart.yaml + docker/Dockerfile.quickstart (pulls a pinned, SHA-verified v277.0.0 release binary; SQLite + localfs + loopback + the MAIDAN_ALLOW_INSECURE_NO_AUTH ack; runs non-root) + scripts/quickstart-two-agents.sh. Built + run end-to-end locally (image builds, /health reports v277.0.0, no SQLite lock thanks to 277, the two-agent demo passes). CI guards the files' validity (compose config + bash -n) in the compose-smoke job. Follow-up: a full run-the-demo CI smoke (source-built server on SQLite/no-auth) — deferred to avoid a network/distroless-perms-flaky job | |
| ✅ Done (279) | maidan init for clean bootstrap | Prod image is --no-default-features (bootstrap routes stripped) → "need an admin token to create the first admin token". Shipped maidan init: connects + migrates, creates the first workspace + admin member (via the *_with_event store methods) + mints an all-capabilities token (new capability::all()), prints the secret once to stdout, and refuses if the store already has a workspace. Removes the need for public bootstrap HTTP routes or AUTH_DISABLED in production. Documented in Production.md; guarded by a maidan-cli integration test (bootstrap-once / refuse-twice) | |
| ✅ Done (arc 282–289) | A2A v1.0 compliance | The A2A endpoint was an experimental Maidan subset. User chose the full multi-transport + TCK scope. Grounding in the authoritative spec (a2aproject/A2A a2a.proto + §5.3 mapping) corrected the backlog's premise: the JSON-RPC method strings are the canonical operation names (SendMessage, not message/send), the TASK_STATE_* enum already conforms, and an Agent Card already exists — so the real gaps are narrower than "everything" | 282 ✅ JSON-RPC method names to spec (CancelTask, {Create,Get}TaskPushNotificationConfig; dropped non-spec tasks/resubscribe). 283 ✅ ListTasks (RBAC-filtered) + GetExtendedAgentCard. 284 ✅ per-task push-config model + all four push-config ops (Create/Get/List/Delete). 285 ✅ Agent Card §4.4.1 schema (supportedInterfaces + capabilities/skills/provider/modes). 286 ✅ HTTP+JSON/REST binding (§11): 9 request/response routes under /a2a/v1. 287 ✅ gRPC binding (§10): tonic A2AService on a config-gated port, vendored codegen. 288 ✅ transport negotiation (§5.2): configurable absolute-URL + gRPC AgentInterface entries. 289 ✅ interop conformance client (examples/a2a_interop.py) + harness + report-only CI job; live-verified. ARC COMPLETE. Follow-ups (logged, non-blocking): gRPC SendMessage/push/streaming, streaming REST endpoints, full A2A error-taxonomy alignment, Helm/compose gRPC port, Agent Card optional fields (securitySchemes/signatures/iconUrl), an official a2a-sdk/TCK-based CI (vs the hand-written conformance client). Deferred within-arc: gRPC SendMessage/push/streaming/extended-card + Agent Card gRPC interface (→288), streaming REST endpoints, A2A error-taxonomy alignment, old workspace-level push table cleanup, push-config token/authentication fields, list pagination, Agent Card optional fields, absolute interface URLs. Plan in scratchpad a2a-v1-arc-plan.md |
| ✅ Done (280) | LangChain + AutoGen recipes | Shipped copy-paste, live-verified recipes: examples/{langchain,autogen,rest}_maidan.py + docs/Framework Integrations.md. LangChain (MultiServerMCPClient) and AutoGen (StreamableHttpServerParams + mcp_server_tools) each load all 78 tools against a running Maidan. Baked in the mcp>=1.9,<2 pin (SDK 2.x drops modules the adapters import) and fixed the one untyped catalog param (set_thread_result.result) so AutoGen's strict converter accepts the whole catalog. Follow-up (P2): a required interop CI job (init → list tools → one read → one write → denied-channel) — deferred as network/adapter-version-fragile; the guide's "Keeping these honest" section prescribes manual re-verification before a pin bump | |
| ✅ Done (281) | Published benchmark methodology | Shipped docs/Benchmark.md (published) + a post_to_observer_latency measurement in the loadgen harness. Reproducible numbers on named hardware/commit/backend: Apple M3 Max / in-process SQLite (one connection) → post→observer p50 0.71 ms/p99 1.00 ms; mixed throughput 1 586 ops/s (8 workers) / 666 ops/s (32, the single-writer SQLite ceiling), zero errors. Also fixed the harness to benchmark the shipped 1-connection SQLite default (was 16 → the Cluster-277 deadlock). Follow-ups: a first-class in-harness Postgres testcontainer target (multi-writer numbers beside SQLite; benchmark-able today via MAIDAN_LOADGEN_URL against a running Postgres deployment); a real embedding-provider latency axis | |
| ✅ Done (292) | Architecture docs currency + split | Split into a current, version-neutral conceptual Architecture.md + Architecture-history.md (release-by-release record). The conceptual doc was also refreshed for currency (it had gone stale ~v104 — the agentic task layer, notifications, three-transport A2A, LSN read-replica, and per-channel RBAC now described); no vX.0.0/cluster vocab on the first user-facing page | |
| ✅ Done (293) | GitHub metadata + repo polish | Set the repo homepage (published docs site) + 10 topics (rust, multi-agent, mcp, model-context-protocol, a2a, ai-agents, agent-infrastructure, agentic, postgres, websocket) via gh; added .github/ISSUE_TEMPLATE/ (bug / protocol-compat / benchmark + config). Follow-up: a terminal GIF / screenshot for the README + repo card (needs a recorded asset) | |
| ✅ Done (313) — L1 / F4 | Default-secure quickstart | The quickstart taught AUTH_DISABLED as the happy path ("one AUTH_DISABLED screenshot kills the launch"). Now compose.quickstart.yaml runs auth ON (dev MAIDAN_SESSION_SECRET + MAIDAN_BOOTSTRAP=1); the README mints a bearer token via maidan init and runs the two-agent demo with it; scripts/quickstart-two-agents.sh is auth-aware (MAIDAN_TOKEN/MAIDAN_WORKSPACE). AUTH_DISABLED demoted to a labelled local-only appendix (compose.quickstart.insecure.yaml). Quickstart image bumped v277.0.0→v312.0.0 (re-pinned tarball SHAs; maidan init landed v279). Both paths validated end-to-end; CI validates both compose files | |
| ✅ Done (314) — L3 / L4 / L5 / L6 | Launch honesty: claims sheet, policies, release verification | Writing the claims sheet caught a real bug — the README headline one-liner didn't boot (auth on needs a ≥32-byte MAIDAN_SESSION_SECRET); fixed + verified. Shipped docs/Claims.md (published, README-linked) mapping every claim → gate/test/"not yet"; a keyless-cosign "Verifying a release" section in SECURITY.md; CHANGELOG-highlights.md + a Release-notes template; and reconciled CONTRIBUTING.md to the solo-maintained/admin-merge model. All launch-prep is done (313 F4 + 314 L3–L6). The public launch itself stays gated on the maintainer's explicit go (Launch.md). |
Code-backed talking points the review validated (use for the launch narrative — all
shipped, honest): the self-healing Postgres NOTIFY floor (pointer signal + durable log +
gap backfill, Cluster 258), workspace-sharded Tokio fan-out (Cluster 201), LSN
causality-token replica reads (Clusters 261–266), and typed non-interchangeable IDs +
SQLite/Postgres Store parity. Positioning moved off "Slack for agents" to the durable
shared-workspace framing (Cluster 274).
Adoption & ecosystem (deferred / post-launch)
Folded here from the concurrent agent's adoption/SDK strategy pack (Cluster 291,
2026-08-27) so Open Work stays the single backlog source. The detailed specs live in
docs/Adoption.md (the funnel + hosted playground/cloud + client program), docs/Clients.md
(SDK implementation plan), docs/Client Contract.md (the frozen v1 SDK surface), and
docs/Client Testing.md (black-box scenarios that double as server coverage) — those are
the spec/index behind these items, not a competing backlog. All gated: none of this
starts without an explicit go.
| Pri | Item | Notes |
|---|---|---|
| ✅ P1 (adoption) — DONE + PUBLISHED | Language SDKs (TypeScript → Python → Go → Rust) | REST + WebSocket clients under sdk/, independent SemVer from the server (publish only on an sdk-* tag). Frozen v1 method surface = docs/Client Contract.md; black-box scenarios (which also catch server bugs) = docs/Client Testing.md. TypeScript ✅ (Cluster 294, 0.1.0) — dependency-free Client (REST + WS), full .d.ts, MaidanError, subscribe/waitFor*, verified black-box (5/5) via scripts/sdk-test.sh. Python ✅ (Cluster 295, 0.1.0) — dependency-free (stdlib urllib REST + a hand-rolled RFC-6455 WS), snake_case surface, verified black-box (5/5). Go ✅ (Cluster 296, 0.1.0) — dependency-free (stdlib net/http REST + a hand-rolled RFC-6455 WS), service-struct surface, verified black-box (go vet/gofmt clean). Rust ✅ (Cluster 297, 0.1.0) — standalone crate (no maidan-* dep; small sync ureq/tungstenite stack, since std has no HTTP/TLS), service-handle surface, verified black-box (clippy -D warnings/fmt clean). THE SDK ARC (294–297) IS COMPLETE — TS, Python, Go, Rust at 0.1.0. Remaining follow-ups: (a) registry publishing — machinery DONE (Cluster 298): sdk-release.yml publishes on sdk-{ts,py,rs,go}-vX.Y.Z tags; NPM_TOKEN/PYPI_TOKEN/CRATES_TOKEN repo secrets loaded; all four dry-run-verified. Remaining: push the sdk-*-v0.1.0 tags to actually publish (docs/SDK Release.md), then confirm the packages resolve. (b) SDK interop CI — DONE (Cluster 299): a report-only sdk-interop job boots a server and runs all four black-box suites via scripts/sdk-test.sh; (c) typed response models (all four currently return generic JSON) — still open (0.2). Rust client must NOT depend on maidan-server. MCP stays a URL (the LangChain/AutoGen door, Cluster 280), not a 4th library; A2A stays a recipe (examples/a2a_interop.py, Cluster 289). ✅ PUBLISHED (verified 2026-08-28): all four are LIVE at 0.1.0 — PyPI maidan 0.1.0, npm maidan 0.1.0, crates.io maidan 0.1.0, sdk-go-v0.1.0 module tag; all four sdk-release runs succeeded 2026-08-27 (secrets loaded). A second 0.1.0 upload is rejected. Remaining is only typed DTOs (0.2) + the sdk/README.md "0.0.1 name-hold" doc lie (→ 316 scrub). |
| P2 (adoption) | Hosted playground (maidan.world/play) | A try-it sandbox: ephemeral workspace + the two-agent hero loop (Cluster 278) in the browser. Detail in docs/Adoption.md §3 |
| P3 (adoption) | Hosted cloud (managed Maidan) | Later; multi-tenant hosting. docs/Adoption.md §4 |
| P2 | SDK interop CI | A CI job running the docs/Client Testing.md scenario catalog across the SDKs once they exist (the report-only A2A interop job, Cluster 289, is the pattern) |
Standing risks (still open)
- Channel/thread authorization — CLOSED (arc 159–165): enforced on read/write (REST+MCP), events (WS+MCP SSE), management (
channel:admin), and references. Historical detail: for REST (160):channel_members(159) +ensure_channel_accessgate every REST content route + search + workspace-context (private channels need a membership row; public +__dm__unchanged; creator auto-added). Surfaces: MCP point-access tools enforced (161); MCP aggregate reads filtered (162); WS/MCP subscribe grants verified against membership (163);reference.rsgated (165); thechannel:adminmembership-management API shipped (164); the A2A JSON-RPC ingress (POST /a2a/v1/rpc) now channel-gated on post + task-read (179). DM generic-route participant gap CLOSED (180) —ensure_thread_access→ensure_dm_participant(verifiedmaidan-auth/src/access.rs); subscribe-grant self-assertion CLOSED (grants verified againstchannel_is_member,subscribe_grants.rs). Optional Postgres RLS defense-in-depth deferred (needs a per-connection GUC refactor on the sharedPgPool; ADR in Decisions.md, Cluster 216). Legacy/members/:id/mentions+/inboxself-only: assessed in 315 — the "session can read another's inbox" concern was a FALSE POSITIVE (bearer-only routes, no/ui/apimount → sessions get 401; bearers are act-as-any by design). Defensiveensure_acting_memberguards added anyway (no-op today; future-proofs a/ui/apimount). - At-most-once event bus (default path) — transactional outbox (10), quarantine (12), HTTP outbox replay (56); NOTIFY duplicates/gaps possible on the optimistic path. Mitigated: opt-in
at_least_oncereconcile delivery (WebSocket 125, MCP SSE 126) is gap-free + at-least-once perconsumer_id. - Bootstrap /
AUTH_DISABLED— high-impact misconfiguration. Mitigated: fail-closed (157) —AUTH_DISABLEDneeds the explicitMAIDAN_ALLOW_INSECURE_NO_AUTHack and refuses boot otherwise (and always in production); compile-time strip (91) removes the path entirely in hardened (--no-default-features) builds. - Indexer staleness — opt-in
INDEXER_STALE_SECS. - PostgresBus listener — best-effort recovery;
/health/readyreflects errors. - SQLite semantic search — brute-force cosine fallback; optional
sqlite-vecfeature for an index; HNSW is Postgres-only (by design, not a gap). hash-v1default —openai-compatibleprovider (v117) gives real semantics;hash-v1is the offline/dev default, not semantically meaningful. → Cluster 315 adds a bootwarn!so a stranger who leaves it unset isn't silently served near-random "semantic" results.rsaadvisoryRUSTSEC-2023-0071— ignored (RS256 id_token verify via openidconnect v4; no fixedrsa); clears on openidconnect v5 (unreleased). See Dependencies.md.- No
v93–v100tags — clusters 93–101 shipped as one batch (PR #264), released asv101.0.0; not a backlog. All four gate tags (incl.maidan-operator-1.0) are cut.
Shipped (reference)
| Ladder / tag | Highlights |
|---|---|
| 17–27 | MCP fan-out, SQLite semantic, Helm server, purge, streamable subset |
| 35–58 | maidan-2.0 product gate — DMs, webhooks, slash, FSM, erase, quotas, completion e2e |
| 59–67 | Agent Integration, streamable TTL, A2A card, outbox ops, app OAuth, context |
| 68–76 | Automation DLQ, capability map, vault truth, A2A subscribe, MCP context, agent gate — Retros/Cluster 76.0 |
Release signing: cosign keyless sign-blob --bundle over tarballs + SBOM in release.yml (automated; was previously manual).
Still deferred (no separate owner)
| What | Notes |
|---|---|
| Multi-region active-active | Out of scope |
Closed (verified v126/v131/v132/v144/v148): OpenAPI↔capability map (121), OTLP export + dashboards + e2e (89/90/123), sqlite-vec + per-model embedding tables (85/86); webhook+automation delivery unification — substantially addressed (shared signing/backoff + unified operator API; storage intentionally separate, 131); global cross-workspace admin audit query API (GET /operator/audit, gated by audit:read-global, 132); docs link-checker in CI (mdbook-linkcheck gate, 144); full MCP streamable transport spec-completeness (version negotiation + header + batching + notifications + GET SSE + Accept + resumability + server→client requests, arc 145–148).
Known state
- Latest tag:
v314.0.0(post-gate hardening, Phase XXIV). Since v273: MCP2026-07-28(300–303), durable mail retry (304–306), Slack + GitHub projectors (307–312), the SDK arc published at 0.1.0 (294–299), and launch-prep (313 default-secure quickstart, 314 claims/policies/release-verification). Next: the 2026-08-28 sweep's 315–318 + fidelity/context flagship arc (above). (Narrative below is the historical v273 program record.) Post-v155 four-arc program complete (156–178). Security-led four-arc program: Arc A (security & correctness) COMPLETE (179–184); Arc B (multi-tenant SaaS ops) COMPLETE (185–189); Arc C (agentic task-queue depth) COMPLETE (190–197). Arc D — performance & scale: tractable perf wins DONE — 198 load/soak harness (scripts/loadgen.sh+#[ignore]dload_baseline), 199 concurrent workspace-context assembly (boundedbufferedper-thread builds), 200 filtered-ANN search (RBAC private-channel deny pushed into the query; honorslimit, no leak), 201 workspace-sharded event fan-out (ShardedBroadcast; O(relevant) not O(all)). Arc D remaining items — assessed + deferred, NOT abandoned:- Batched
pg_notify— DECLINED (low value + delivery-core risk). The LISTEN handler hydrates a single pointer per NOTIFY, and the hot path publishes per-event (no natural batch); only the latency-tolerant fallback relay batches. A correct coalescing needs range-hydration on the listener (tracklast_hydrated_log_id, hydrate(last_hydrated, X]per pointer, advance) — a delivery-core change for a win that only helps the non-hot path. Range-hydration alone is a robustness win (self-heals dropped NOTIFYs) if ever wanted, but risks double-delivery without careful last-hydrated tracking. - Read-replica routing — DEFERRED (needs infra + a Store refactor). Requires a second read-pool threaded through the
Store(which is constructed with one pool), read-after-write consistency handling (route reads-after-writes / real-time to primary; only lag-tolerant reads like search/workspace-context to the replica), config (MAIDAN_DATABASE_REPLICA_URL, degrades to primary when unset), and a real replica to validate beyond the degenerate case.
- Batched
- Deferred from Arc C: federation
content→partsegress (194 did ingest; egress still body-only). - Perf follow-ups (surfaced this arc): the workspace-context route builds every page thread then RBAC-filters (build-then-filter wastes work; filter-before-build is a bigger refactor with pagination subtlety); the search deny-set is
list_channels+ a per-channelchannel_is_member(a single "my private channels" query would be cheaper); full DM-at-query-level for search (eliminating the post-filter) deferred (DM participation in SQL is complex). - NEW four-program arc (from a 5-agent sweep, 2026-08-12) — run in order, clusters 202+: (A) Security & correctness round 2 — 202 session-bound acting identity ✅, 203 DM/group-DM participation ✅, 204 cross-tenant artifact isolation (maidan_artifact_refs link table) ✅. Transactional outbox (atomic domain-write + event-append — the 184 deferral;
*_with_eventStore methods in one tx) is a multi-cluster migration: 205 foundation (append_in_tx+ channel/thread create) ✅, 206 votes + reactions ✅, 207 pins + mentions ✅, 208 thread transitions (transition_thread_with_event+thread_scope_in_tx) ✅, 209 thread assignments (assign/unassign/claim/claim_next*_with_event; thread-scoped batch complete) ✅, 210 DM/group-DM posts (post_message_with_event(new, dm_conversation_id)) ✅, 211 the regular (slash-entangled) message post (edit_message_with_posted_eventfor the slash-finalize; no-slash usespost_message_with_event) ✅, 212 message edit + tombstone (message.rsnowpublish()-free) ✅, 213 the A2A ingest post (reusespost_message_with_event) + member/workspace creation ✅, 214 references (add_reference_with_event) + artifacts (upsert_artifact_with_event— folds upsert + the Cluster-204 access ref +ArtifactUpsertedin one tx) ✅. The domain-mutation outbox migration is COMPLETE (205–214) — every event tied to a domain-table write commits atomically with it.publish()correctly remains (no rename/delete): its remaining callers append standalone events with no domain-table row to be atomic with — the federation relay (federation.rsre-publishes remote events onto the local bus) andpublish_routed_mentions(routes/mod.rsfans a durableMentionRecordedto each auto-parsed @mention for realtime routing — nomaidan_mentionsrow, distinct from the explicit-mention-APIrecord_mention_with_eventof 207).publish()= "durably append a standalone event + notify" is the right primitive for both, so the refactor concludes at 214 with no cleanup cluster. 215 federation ingest trust policy ✅ —EventKind::federatable()allowlist enforced at ingest (ArtifactUpsertedexcluded — blobs aren't federated; both push endpoint + pull worker) + fixed theMemberJoinedremap leaking the peer's remotemember.workspace_id. (The "referenced-entity-in-peer-workspace" framing resolved to that nested-workspace re-scope fix; federation is event-log replication, not entity materialization, so there are no local entity rows to validate against.) 216 the RLS spike ✅ — resolved as a decision ADR (docs/Decisions.md## Security): Postgres Row-Level Security assessed + deferred; app-layer RBAC stays authoritative (blockers: shared pool with no per-request tenant binding, workspace-agnosticStoretrait, SQLite has no RLS → parity break, cross-workspace bearer orchestrator model, duplicates an already-comprehensive control; trigger conditions recorded). Program A (security & correctness round 2, Clusters 202–216) is COMPLETE. (B) agentic orchestration (task DAG, scheduled/recurring tasks, capability registry + skill routing, queue depth, coordination waits + structured results) — BEGUN: 217 landed the task-dependency DAG store foundation ✅ (maidan_thread_dependenciesedges +thread_depsstore: add/remove/list-deps/list-dependents/dependencies_satisfied; readiness = all deps terminal; reuses the thread-as-task model; zero-blast-radius, no routes yet — the Cluster-159 pattern). 218 readiness-awareclaim_next✅ (aNOT EXISTSclause skips tasks with non-terminal deps, both backends +_with_event; the existing REST claim-next route + MCPclaim_next_threadtool are now DAG-aware, no new API). 219 DAG-management REST API ✅ (POST/GET /threads/:id/dependenciesadd + list+ready,DELETE …/:dep_id,GET /threads/:id/dependents; both-thread RBAC + same-workspace; full new-route preflight). 220 the MCP DAG tools ✅ (add_thread_dependency,list_thread_dependencies; both-thread RBAC; DAG read/write surface complete over REST + MCP). 221 transitive cycle prevention ✅ (add_thread_dependencyrejects direct + transitive cycles via recursive-CTE reachability, both backends — the DAG is now acyclic). 222 reactive readiness ✅ (ThreadReadyevent on dependency-unblock +newly_ready_dependentsquery, both backends; subscribe withkinds=thread_ready). 223wait_for_readyMCP long-poll ✅ (blocks until a task becomes claimable; thewait_for_mentionanalogue; DAG surface now complete end-to-end). 224 channel queue-depth ✅ (GET /channels/:cid/queue-depth→ ready/assigned/blocked counts; one aggregate query, both backends). 225get_queue_depthMCP tool ✅ (the MCP twin; task-queue subsystem now feature-complete over REST + MCP). 226 scheduled/recurring task foundation ✅ (task_schedulestable + model + store CRUD/due-scan, both backends; zero-blast-radius, no worker/routes). 227 scheduler sweeper worker ✅ (opt-inMAIDAN_SCHEDULER_TICK_SECS;claim_next_due_scheduleatomic claim-and-advance,FOR UPDATE SKIP LOCKEDon pg so replicas don't double-fire; fires a task thread per due schedule; at-most-once on crash). 228 scheduler REST management ✅ (create/list/pause-resume/delete over/workspaces/:wid/task-schedules+/task-schedules/:id;workspace:write+ target-channel access;set_task_schedule_active). 229 scheduler MCP ✅ (create_task_schedule+list_task_schedules; the scheduled/recurring-task subsystem is now complete over REST + MCP). Arc E — capability registry + skill routing opened: 230 member-skills foundation ✅ (member_skillstable + model + store add/remove/list, both backends; zero-blast-radius). 231 skill-aware claim ✅ (thread_required_skills+claim_next/claim_next_with_eventskill-match clause, both backends; existing claim route/tool inherit skill routing). 232 capability-registry REST ✅ (member-skill + thread-required-skill CRUD; 6 routes). 233 capability-registry MCP ✅ (add/list member skills + add/list thread required-skills). Arc E COMPLETE (230 foundation → 231 skill-aware claim → 232 REST → 233 MCP). Deferred: a "capable members for this task" discovery read (members whose skills ⊇ requirements) — optional orchestrator convenience;claim_nextalready routes automatically. Arc F — coordination waits + structured results opened: 234 structured-results foundation ✅ (thread_resultstable + model + store set/get, both backends; zero-blast-radius). 235 Arc F REST + event ✅ (PUT /threads/:id/resultthread:transitionupsert +GET …/resultworkspace:read→404until produced, both DM-participant-aware thread RBAC;ThreadResultSetevent on set — a "go fetch" pointer observable on WS + MCP-SSE likeThreadReady, locally-derived → non-federatable). 236 Arc F MCP ✅ (set_thread_resultthread:transition/get_thread_resultworkspace:read— the twins of 235's REST;wait_for_resultworkspace:read— block on a thread'sThreadResultSet, return the result payload, thewait_for_readyanalogue;get_dependency_resultsworkspace:read— a parent aggregates its dependencies' outputs as[{thread_id, result}], RBAC-filtered; 5-place MCP wiring + both sorted contracts; testresult_tools_set_get_wait_and_aggregate). ARC F COMPLETE (234–236) — and PROGRAM B (agentic orchestration, 217–236) is COMPLETE: task-DAG + queue (217–225), scheduled/recurring tasks (226–229), capability registry + skill routing (Arc E, 230–233), coordination waits + structured results (Arc F, 234–236). Deferred within Program B: a "capable members for this task" discovery read (Arc E note); federation egresscontent→parts(A2A ingressparts→contentshipped 194). Next: Program C (notifications & reach), then Program D (scale & durability). (C) notifications & reach (per-recipient router + inbox, prefs + presence-aware routing, email/SMTP transport, digests, follow/UI) — BEGUN (grounded by a fresh recon of the mentions/webhook/presence/subscribe surface): the gap is that mentions are recorded + polled, never delivered per-recipient; webhook delivery is a single per-workspace firehose keyed on event kind; no prefs/mute/follow;deliver_httpis the only transport. Planned as three arcs (plan in scratchpadprogram-c-plan.md): Arc G per-recipient ledger + router + unified inbox, Arc H preferences + subscription (mute/follow), Arc I transport (email/SMTP) + digests + presence-aware routing +/uinotification center. 237 ✅ opened Arc G with the per-recipient notification ledger foundation (maidan_notificationspg 0042/sqlite 0041 — one row per recipient × source event,kind=EventKind,source_log_idno-FK so it survives retention pruning, denormalizedchannel/thread/message/actor,read_atNULL=unread;Notification/NewNotification+ store CRUD both backends; zero-blast-radius, no router/routes). 238 ✅ notification router (NotificationRouteralways-on reconnecting bus consumer inmain.rs;route_eventresolvesMentionRecorded→mentioned member, channel resolved from thread;create_notification_if_absent+UNIQUE(member_id,source_log_id)index pg 0043/sqlite 0042 → cross-replica/replay dedup;maidan_notifications_created_total{kind}metric; e2enotification_router_e2e). 239 ✅ REST unified inbox (GET /members/:id/notificationslist +…/unread-count+POST …/:nid/read+…/read-all; allworkspace:read+ self-only for sessions viaensure_acting_member, bearer act-as-any;mark_notification_readrecipient-scoped(member_id,id)in the store; full new-route preflight; e2enotifications_inbox_e2e). Follow-up surfaced: the legacy/members/:id/mentions+/inboxroutes enforce onlyworkspace:read+ same-workspace (any workspace member can read another's mention feed) — the Cluster-202/203 self-only hardening never reached them; retrofit them (not done in 239 to keep scope tight). 240 ✅ MCPlist_notifications/get_unread_count/mark_notification_read(twins of 239's REST) +wait_for_notification(general form ofwait_for_mention; sharedwait_for_member_eventhelper; returns the triggering event, ledger backs the drain). ARC G COMPLETE (237–240): per-recipient notification ledger → router → REST inbox → MCP. Remaining Program C: Arc H preferences + subscription — 241 ✅ mute-preferences foundation (maidan_notification_prefspg 0044/sqlite 0043, PK(member_id,kind)+muted;NotificationPref+ store set/list/is_notification_muted; zero-blast-radius). 242 ✅ mute-aware router + prefs REST (route_eventconsultsis_notification_muted→ skip muted(member,kind)+maidan_notifications_suppressed_total{reason}metric;PUT/GET /members/:id/notification-prefsset/list,workspace:read+ self-only). 243 ✅ mute MCP tools (set_notification_pref/list_notification_prefs;kindsnake_case parsed → EventKind; member-scoped, no gate arm) — the mute half of Arc H is complete over REST + MCP. Remaining Arc H = follows/subscription: 244 ✅ foundation (maidan_channel_follows+maidan_thread_followspg 0045/sqlite 0044, presence=following, reverse index;ChannelFollow/ThreadFollow+ store follow/unfollow/list/*_followers, both backends; zero-blast-radius). 245 ✅ follows-aware router + REST (route_eventMessagePostedarm fans tochannel_followers ∪ thread_followersminus author, mute-checked via a sharednotifyhelper; skips DM posts;POST/GET /members/:id/channel-follows+DELETE …/:cid+ thread triple, self-only, follow gated onensure_channel/thread_access). CORRECTION to the earlier note: the dedup index does NOT prevent a mentioned-and-following member getting two notifications —MentionRecordedandMessagePostedare distinct events (distinct log_ids); per-kind mute (message_posted) is the control. Follow-up: the router doesn't skip followers who LOST access after following (pointer-only notification; thread read stays RBAC-gated). 246 ✅ follows MCP tools (follow_channel/unfollow_channel/list_channel_follows+ thread triple;follow_*gate on target access via the pre-dispatch channel/thread arms). ARC H COMPLETE (241–246): mute preferences + follows/subscription over REST + MCP. Remaining Program C: Arc I (transport + reach) — 247 ✅ email/SMTP transport foundation (MailTransporttrait +lettreSmtpTransport+SmtpConfig::from_env; config-gated + unwired;lettreon the rustls+tokio stack,cargo denygreen with0BSDallowed). 248 ✅ recipient-address store (maidan_member_emailspg 0046/sqlite 0045, one per member — separate table to avoid the member-row ripple;MemberEmail+ set/get/delete). ⚠️ 248 also carried an mdbook hotfix: the Cluster-236get_dependency_resultscatalog description used bare[{thread_id, result}], whichgen-mcp-referencerenders intomcp-reference.mdprose where the mdbook linkcheck treats it as an incomplete link (memorymaidan-docs-linkcheck-brackets) → the non-requiredmdbookjob had been RED since 236 (unnoticed because the ship-monitors only watch the 8 required checks). Fixed the description to prose (no brackets). Lesson: also glance atmdbook(+ other non-required jobs) before merging, not just the 8 required. 249 ✅ email delivery wired into the router (AppState.mail+attach_mail, built fromSmtpConfig::from_envinmain.rs; routerdeliver_notification_emailspawned best-effort after the in-app write so a slow SMTP server never blocks routing;maidan_email_delivered_total{outcome}; address-presence = opt-in; recording-transport e2e). Best-effort, no retry (a durable retrying email delivery queue is a follow-up); no address surface yet (set via store only until 250). 250 ✅ member delivery-email REST (PUT/GET/DELETE /members/:id/email, self-only + light@check; email now usable end-to-end over REST). 251 ✅/uinotification center (a "Notifications" tab: list + unread badge + mark-read/read-all + unread-only filter, over/ui/api/members/:id/notifications*routes reusing the 239 handlers under session middleware;sessionMemberId=self;ui_js_contractgreen). 252 ✅ durable member last-seen store foundation (maidan_member_last_seenpg 0047/sqlite 0046,member_idPK +last_seen_at; storetouchupsert-now()/get→Option<DateTime>, both backends — the persistent presence signal presence-aware routing needs since presence is in-memory only; separate table to avoid the member-row ripple, no model type; zero-blast-radius, unwired until 253). 253 ✅ presence-aware email routing — the WS handlertoucheslast_seenon presence registration (at thews.rsregistercall site, NOT inside the store-lessPresenceHub; best-effort + spawned so it never blocks the connect), anddeliver_notification_emailskips the send when the recipient was seen withinMAIDAN_EMAIL_PRESENCE_WINDOW_SECS(opt-in; unset/0 = send as before, Cluster-249 behaviour), meteredoutcome="skipped_present", fail-open on a read error. Wires the 252 store end-to-end. 254 ✅ scheduled-digest data model (store foundation): user chose the alternative-mode product (immediate per-notification emails OR a periodic digest, not both), so this landedEmailDeliveryMode(Immediatedefault /Digest) +DigestDuein maidan-types,maidan_member_delivery_prefs+maidan_member_digest_state(pg 0048/sqlite 0047), and storeset/get_delivery_mode(default Immediate on absence) /set_last_digest_at(digest watermark) /members_due_for_digest(digest-mode members w/ address + unread-since-last-digest, address inline), both backends — zero-blast-radius, unwired. 255 ✅ wired it — the router skips a digest-mode member's immediate email (deliver_notification_emailearly-returns onget_delivery_mode == Digest, meteredskipped_digest), and an opt-in digest sweeper worker (digest.rs,MAIDAN_DIGEST_TICK_SECS, Cluster-227 sweeper shape) drainsmembers_due_for_digest, emails an unread-count rollup viastate.mail, and advancesset_last_digest_atonly on a successful send (at-least-once, self-healing — a transient failure retries next tick). No-op without a transport; deliberately NOT single-flighted across replicas (a duplicate digest is low-harm, unlike the scheduler's harmful double-fired task — run on one replica for exactly-once). Alternative-mode digest works end-to-end. 256 ✅ delivery-mode REST —PUT/GET /members/:id/delivery-mode(workspace:read+ self-only viaensure_acting_member, the notification-prefs cap model);SetDeliveryModewrapsEmailDeliveryModeso an unknown mode is a400at the extractor;GETis total (immediatedefault, no 404); full new-route preflight (OpenAPI +EmailDeliveryMode/SetDeliveryMode/DeliveryModeViewschema regs + capability-map + matrix PUT body clause). 257 ✅ delivery-mode MCP tools (set_delivery_mode/get_delivery_mode,workspace:read, member-scoped, no gate arm — the notification-pref tool shape;setparses snake_caseimmediate/digest→InvalidParamson unknown, both return{mode}) — the twins of the 256 REST. The core of Arc I is complete (transport 247 → address store 248 → router wiring 249 → address REST 250 →/uicenter 251 → presence-aware routing 252–253 → digests 254–257). Remaining Arc I: optional MCP email-address tools for parity (low value — email is human-facing config). Arc I (email/SMTP transport + digests + presence-aware routing +/uinotification center). (D) scale & durability — BEGUN (user chose the NOTIFY floor first, 2026-08-21): 258 ✅ event-bus self-healing NOTIFY floor (maidan-bus/postgres.rs: high-waterlog_id+drain_new_eventsback-fills the missed range from the log on a gap (pointer id >high_water+1→ back-fill the exclusive middle) or a listener reconnect (drain to head); always single-hydrates the pointer's own id so a concurrent late-lower id isn't dropped; batched/best-effort;list_after_global/max_event_idcross-workspace log reads;Backfilledstat +{result="backfilled"}metric;PostgresBus::backfillheal hook. Optimistic-path resilience — the outbox + at-least-once cursor stay the durable path). 259 ✅ chaos / fault-injection harness (crates/maidan-bus/tests/chaos.rs+scripts/chaos.sh): an#[ignore]d soak publishes under load while killing theLISTENbackend (pg_terminate_backendonLISTENconnections), asserting published ⊆ delivered — validated the 258 floor end-to-end (40/40 delivered across 5 kills, 0 missing). Purefault_duecadence helper unit-tested in CI; soak is a manual tool likeloadgen. 260 ✅ backup/restore + DR runbook (scripts/backup.sh=pg_dump -Fc+ tar of the localfs artifact root + manifest;scripts/restore.sh=pg_restore, refuses a non-empty target without--force; a "Backup & disaster recovery" section indocs/Production.mdcovering out-of-band secrets, S3-is-durable, RPO/RTO, recovery steps — operator tools likeloadgen/chaos,bash -n-clean, not CI-gated). Read-replica routing — IN PROGRESS (user chose the full LSN causality-token design, 2026-08-22: strong read-your-writes, multi-cluster, validated against real streaming replication; plan in scratchpadread-replica-plan.md). 261 ✅ LSN primitives + replication harness (validate-first keystone):Lsntoken type (maidan-types, u64-backed for correct numeric ordering) + storecurrent_wal_lsn/replica_replay_lsn/replica_caught_up(postgres::replication, direct-call likeget_by_id) +scripts/replica-harness.sh(proven local pgvector primary+standby recipe — pg_hbahost replicationline + standbypg_basebackup -Ras the postgres user) + an#[ignore]d test validating the helpers against real replication (passed). Inert — no read routed yet. 262 ✅ reader-pool split (PostgresStore { pool, reader }+with_replica_reader;newdefaults reader=primary so no ripple to ~62newsites;MAIDAN_DB_REPLICA_URLconfig + boot wiring connects a real reader pool, fail-fast on a bad URL, shared connection setup; reads still on the primary — inert until 264;readerfield#[allow(dead_code)]until the selector). 263 ✅ consistency token on writes:Store::write_lsn()(Postgrespg_current_wal_lsn(), SQLiteNone) +AppState.read_replica_enabled(main.rs fromMAIDAN_DB_REPLICA_URL) +consistency::middlewarestampingMaidan-Consistency-Token: <lsn>on successful mutations, captured after the handler (safely over-approximating — never behind the write), gated on a configured replica (no replica → no token, no round-trip). 264 ✅ token ingestion + read routing:READ_CONSISTENCYtask-local +with_read_consistency(GET/HEAD-only scope, so mutation/background reads stay on the primary — no read-then-write staleness) +read_pool()/pureroute_decision+ a background poller caching the replica'spg_last_wal_replay_lsn()in an atomic (stale cache is safe — only false-routes to primary) + entity-read delegations (workspace/member/channel/thread/message get+list) routed to the replica once it has replayed past the client's token, else primary. Validated vs real streaming replication (read_routingignored e2e passed: read-your-write holds, replica serves no-token reads). 265 ✅ routed the remaining content/collaboration read families (28 delegations: skills/results/notifications/follows/emails/last-seen/channel-members/dm/group-dm/transitions/queue-depth/schedules/assigned/deps/edits/mentions/inbox/votes/reactions/usage) +maidan_replica_reads_total{outcome}(store-sideReadRoutingMetrics). Auth-path reads (sessions/tokens/oidc/peers) + control-plane/config reads (webhooks/slash/fsm-hooks/deliveries/reindex/audit/quotas) deliberately stay on the primary (auth middleware runs on GETs → a lagging replica would break just-minted creds). Validated vs real replication (routing counters assert both outcomes). 266 ✅ replica-lag gauge (maidan_replica_lag_bytes— poller samples primary write LSN too →current − replay) + Production.md "Read replicas" section (config,Maidan-Consistency-Tokencontract, routing policy, metrics, harness). The LSN read-replica arc (261–266) and PROGRAM D (scale & durability) are COMPLETE — and with them the entire security-led four-program run (A 202–216, B 217–236, C 237–257, D 258–266). Optional-deferrals sweep IN PROGRESS (user chose: import BOTH modes, search HONOR-the-token; 2026-08-24 — scratchpaddeferrals-plan.md). 267 ✅ A2A egresscontent→parts(message_parts_from_contentegress inverse + the A2A agent renders its outbound message from the stored message's canonical content, not an echo; federation event-relay already carried content). 268 ✅ MCP email-address tools (set/get/delete_member_email, parity w/ 250 REST over the 248 store). 269–270 ✅ workspace import (both modes: new-workspace-remap default /?mode=restoresame-id,&forceerases first) —Store::import_workspace+POST /workspaces/import. 271–272 ✅ search token-aware read routing (PostgresSearchreader pool + replay poller + sharedmaidan_store::postgres::replica_route;maidan_search_replica_reads_totalmetric) — validated vs real streaming replication. The optional-deferrals sweep (267–272) and the LSN read-replica program are COMPLETE. Transactional outbox already DONE (shared w/ Program A, 205–214). Full per-lens detail in the session's workflow journalwf_b8cdaaa2-be4. Next forward work → see "Post-272 forward work" below. - Assignment queue follow-ups (Clusters 190–192): MCP tools shipped in 191; claim leases + reclaim shipped in 192. Remaining:
claim_nextis channel-scoped (no workspace-wide pull); no server-side default lease (the caller setslease_secs); reclaim is lazy (only a subsequentclaim_nextfrees an expired lease — nothing actively unassigns a dead holder / emits an event until someone pulls). - Secret-rotation follow-ups (Cluster 189): migration to a rotated key is lazy (a secret moves only when re-saved — no bulk re-encrypt sweep, so an old key must stay in
FEDERATION_DECRYPT_KEYSuntil all secrets rotate); the fallback set is a startupOnceLock(rotation needs a restart, not a live reload). - Usage/metering follow-ups (Cluster 188): no per-tenant storage bytes (content-addressed artifacts dedup across workspaces — attributing by uploader would double-count; decide a convention if billing needs it); usage is a point-in-time snapshot (no historical time-series — operators sample on their cadence).
- Workspace export follow-ups (Cluster 187): reactions/votes not exported (per-message N+1); artifact blobs not included (metadata via references only); the bundle is built in memory + returned in one response (a streaming/NDJSON variant would scale better). (Import path SHIPPED 269–270 —
Store::import_workspace+POST /workspaces/import, both new-workspace-remap and?mode=restore; the "no import path yet" note is resolved.) - Retention follow-ups (Cluster 186): no
occurred_atindex on the pruned tables (the daily batched sweep tolerates a scan; add if it gets hot); deliveries prune is lightly tested (valid-query/empty smoke — the FK fixture for delivery rows was deferred); a stale/abandoned delivery cursor pins the event-log prune floor (needs a stale-cursor reaper eventually). - Denial (401/403) auditing → logs/metrics, not the audit table (Cluster 182 decision). Table-level per-denial auditing is an attacker-controlled, unbounded
maidan_auditwrite amplifier. If durable denial history is ever needed, do it in a sampled/rate-limited sink separate from the audit table. - True single-transaction dual-write atomicity — ✅ DONE (transactional-outbox migration 205–214). (Corrected 2026-08-28: this was listed as open, but the migration completed it.) Every event tied to a domain-table write now commits atomically with it via
*_with_eventstore methods sharing one tx (verified e.g.postgres/channels.rscreate_with_event=begin → append_in_tx → commit), including the slash-entangled message-post path (edit_message_with_posted_event).publish()correctly remains only for the two callers that append standalone events with no domain row to be atomic with (the federation relay +publish_routed_mentions). - Deferred (Cluster 173): federation/A2A-ingested messages carry
bodyonly — the ingest path (a2a_agent.rs, federation worker) doesn't yet map incomingparts → content(typed structured content). In-scope-to-not-break; propagation is a follow-up. All four gate tags cut (maidan-2.0v58,maidan-agent-1.0v76,maidan-operator-1.0v101,maidan-scale-1.0v120). - Active work: post-gate hardening clusters (121+); no further ladder gate defined. See Roadmap + Remaining Work.
- Integrators: start at Agent Integration and
contracts/.
How to read this file
- Remaining Work — partial implementations + Slack matrix.
- Roadmap — cluster pointer and historical closes.
- Retro PRs are the right time to add or remove deferrals.