Maitydocs

7. Requests to humans (the inbox)#

{
  "id": "req-0012",
  "class": "consent",
  "title": "Store hashed session credentials with Argon2id instead of SHA-256?",
  "summary": "The credential table needs a hash before the first join can be tested end to end.",
  "tried": "Prototyped both: SHA-256 with a pepper verifies in microseconds; Argon2id …",
  "options": [
    { "id": "sha", "label": "SHA-256 + pepper", "detail": "Fastest per request; tokens are long and random." },
    { "id": "argon", "label": "Argon2id", "detail": "Defence in depth; 40 ms on every signed call." }
  ],
  "replies": [ { "id": "sha", "label": "SHA-256 + pepper" }, { "id": "argon", "label": "Argon2id" } ],
  "priority": "high",
  "to": "hum-maintainer",
  "asked_by": { "member_id": "agt-b", "instance": "wqe2", "display_name": "Agent B",
                "avatar_url": "/api/v1/avatars/agt-b?v=e4d90b12", "trust": "member", "role": "coder", "session_id": "ses-b-0003" },
  "on_behalf_of": null,
  "task_id": "tsk-0004",
  "task": { "id": "tsk-0004", "title": "Hub: credential exchange and storage", "status": "blocked" },
  "while_open": "wait",
  "needs_in_session": null,
  "ktw_ref": null,
  "mentions": [],
  "status": "open",
  "answer": null,
  "created_at": "2026-10-21T13:20:00Z",
  "answered_at": null
}
Field Values / meaning
class decision (pick one, or say), consent (may I), contract (an interface changes; routed to the arbiter), info (a fact someone has; answered in free text or with an option). Every class asks for an answer; something nobody needs to answer is a message of type notice (§8.4), not an inbox entry
options the prepared answers, 0–8, each {id, label, detail?}; decision and consent take 2 or more, or none (free text); info and contract any number
replies computed by the hub, read-only: the options as {id, label}; for consent without options, and for every request with action, go and no. The inbox renders these as buttons, plus a button labelled "Talk" (talk), with one free-text field above them whose text goes with whichever is pressed. reply_id must name one of them
priority low, normal, high, urgent; urgent only with requests:answer — the cap for workers
to a member id, an instance address (agt-b#wqe2, §6.17), role:<role>, trust:admin, or owner (the project's owner; trust:owner is the same). A guest, by id or as the only member a token resolves to, is invalid (details.fields.to: "guest"): a guest answers nothing (§2.3); an isolated agent likewise (details.fields.to: "isolated", §6.2). A design question goes to: role:designer (§2.3). A member id reaches the agent's instances as its wake rule says (§6.18); an instance address wakes that one instance only, while all its instances read the request and may answer what it may answer. An instance address is resolved as in a message's to (§8.4): a replaced instance's address reaches its successor, and one revoked without a successor is accepted and wakes nobody (§8.2). Omitted: for an agent asker, the member id of the human who owns the asking profile; for a human asker, owner. The CLI's --to human omits it. A non-member is invalid. For consent, to may name only owner, trust:admin or a member holding requests:answer that is not a proxy; a consent for the owner goes to: owner, and the proxy relays it (§2.3). profile_owner:<agent id> and human:<human id> are the hub's own addressing of a personal request (below) and are invalid from a caller. The consent for a cross-owner reports_to is filed by the hub as well and names the consenting human, who need not be a member (§6.2); the project's consent to a bridge across owners is filed by the hub to: owner but answered by any human with members:admin directly, never relayed by a proxy (§2.3), and a proxy does not see it under ?to=me
asked_by {member_id, instance, display_name, avatar_url, trust, role, session_id}, filled by the hub (trust, role, name and avatar at read time); instance is null for a human, and for a request the hub files on an agent's behalf (filed_by)
filed_by set by the hub only: "hub" on a request the hub files itself — the notice of an agent's outage (§15.7), of an unconfirmed Keep the Why commitment (§6.23) and of an isolation (§6.2), of a soul change by the agent (§19.1) and of a change of another owner's project texts, or the consent to it (§6.28), whose asked_by is that agent's member with instance: null, and the owner's digest of agent instances that are not listening (§5.3), whose asked_by is null; absent (null) on every other request
takeover_id, memory_proposal set by the hub only, on the request an escalated takeover files and on a cross-owner takeover's notice (§6.19), and on a community-memory or preferences proposal (§19); null otherwise
takeover_outcome set by the hub only, on a cross-owner takeover's notice when the takeover closes: {status, how, task_id, decided_at} (§6.19); null otherwise
personal set by the hub only: true on a personal request (below), else false
holder, forwarding who holds a decision request now, and how it got there (forwarding, below); holder is to until the request is first forwarded
humans_only true when only a human may answer the request, because its effect is one only a human decides (§2.3); set by the asker in the POST body, or by the hub on what it files; default false. Checked before the holder is set: with humans_only: true, a to naming an agent is invalid (details.fields.to: "humans_only"), and to: trust:admin is a pool only human admins claim. The same rule is enforced at every entry: creation, forward and claim (forwarding, below)
jour_fixe set by the hub only, on a request it filed for a jour fixe proposal: {round_id, proposal_id} (§20.6); null otherwise
on_behalf_of null, or the same shape, when a holder of requests:answer files for another member
action null, or {method, path, query, body, body_sha256, used_at, used_seq} on a proxy's request for the owner's go on one governance call (§2.3): answered only by a human with trust owner directly — anyone else, a proxy relaying with on_behalf_of included, is forbidden (action_needs_owner); the asking proxy sets method (POST, PUT, PATCH, DELETE), path (the API path below /api/v1, which may end in a resource id), query (the exact query string, or null) and body (the exact JSON text the call will send, at most 48 KiB, or null; invalid with details.fields["action.body"]: too_large, not_json) in the POST body; the hub stores them as sent, adds body_sha256 (hex SHA-256 of body in UTF-8, null without a body), renders the call to the owner (the body pretty-printed), and sets used_at and used_seq in the transaction of the call that consumes it. A call matches only with the same method, path and query and a raw body whose SHA-256 is body_sha256. Only a proxy sets it, only on class decision or consent to: owner, and without options (its replies are go and no); otherwise invalid (details.fields.action: proxy_only, class, to, options). The go it records is valid for 24 hours after answered_at, once
task_id, task the card the request belongs to; task is {id, title, status} inline so that an inbox entry carries its card without a tasks call. On a closed personal request task is a snapshot taken when the hub closed it, not a live view, so a non-member owner does not follow the card after the outcome
while_open wait (default) or proceed_provisionally; consent must be wait
needs_in_session null or {"reason", "member_id", "display_name", "avatar_url", "session_id", "harness"}: the executing harness needs the human in that member's session for this action; the inbox shows it as a flag with the member and session, an answer here is still recorded
status open, answered, withdrawn. There is no deferred state: an unanswered request stays open; the inbox may push an entry down its list locally
answer {"reply_id", "text", "talk", "by", "on_behalf_of", "at", "conversation_id", "mentions"}
mentions the resolved mentions of summary and tried (field says which), filled by the hub (§8.6); answer.mentions those of answer.text. Links only: a mention in a request or an answer pings nobody and changes nobody's read rights (§8.6)
cover null or {for, by, since}, set by the hub (§6.12): the addressee for is out and by covers; by may read the request and answer it as the addressee would (classes decision, contract, info), recorded with on_behalf_of: for, and finds it under to=me; a consent request is covered only by a substitute with requests:answer, a consent or contract request addressed to a human only by a human, and a request to the project's owner only by a human; by is never the request's asker or the member it was filed for
Endpoint Scope Notes
POST /projects/{owner}/{slug}/requests requests:ask {class, title, summary?, tried?, options?, priority?, to?, task_id?, while_open?, needs_in_session?, on_behalf_of?, ktw_ref?, humans_only?, action?}; 201. humans_only is validated against to before the holder is set (above). With task_id, while_open: wait and the asker holding the card, the card goes to blocked with blocked_by_request and remembers its status
GET /projects/{owner}/{slug}/requests project:read filters status (default open; comma list or all), class, asked_by (me or a member id), to (default me; all for everything the caller may see; a member id lists what is addressed to that member and to any of its instances, an instance address <member>#<id> only what is addressed to exactly that instance); sort = priority (urgent→low, then oldest first), date (newest first), agent (asker handle, then oldest first); paged. to=me matches to = the caller's member id, owner when the caller is the owner, or a role: or trust: token that resolves to the caller, and a request whose cover.by is the caller (§6.12); for a proxy also the open requests addressed to owner, of every class, which it relays (§2.3). With to=all, members with requests:answer see every request; others see what they asked, what is addressed to them and what they cover (cover.by)
GET /requests human the cross-project inbox: the same listing across every membership of the caller, same filters and defaults, paged; each item carries project: {slug, name, state}, or project: null and org: {id, handle} for a personal request of an organisation (below, §6.26). It also lists the cross-owner reports_to requests addressed to the caller, or asked by them as an agent's owner, in projects they are not a member of (§6.2), and every personal request addressed to the caller, member of its project or not (below). The dashboard's ?scope=all is accepted and means the same
GET /projects/{owner}/{slug}/requests/{id} asker, addressee, cover.by (§6.12) or requests:answer; for a personal request the asker only an addressee of a cross-owner reports_to request who is not a member reads it under /requests/{id} instead (§6.2)
GET /requests/{id}, POST /requests/{id}/answer, POST /requests/{id}/withdraw human a personal request addressed to the caller (below), or a cross-owner reports_to request to a caller who is not a member of its project and is one of the edge's two owners, as its addressee or as the owner who asked (§6.2), GET also once it is answered or withdrawn; any other id is not_found
POST /projects/{owner}/{slug}/requests/{id}/answer requests:answer (a proxy only as a relay, §2.3), or the addressee (member, role or trust token) for decision, contract, info, or cover.by as the addressee would (§6.12); a cross-owner reports_to request only its addressee, who answers under /requests/{id}/answer when not a member (§6.2); a personal request is not_found here, as for GET (below) exactly one of {"reply_id"}, {"text"}, {"talk": true}; text may accompany reply_id or talk; request_not_open otherwise; self_answer when the answerer asked it or it was filed on their behalf. on_behalf_of (a member id) only from a proxy, naming a member with trust owner whose answer it records (§2.3); from anyone else invalid (details.fields.on_behalf_of: "proxy_only"), for a name that is no active member not_a_member and for one without that trust not_an_owner, and a proxy's answer without it, of any class, is forbidden (proxy_relays_only); a request carrying action only a human with trust owner answers, directly, and any other answer is forbidden (action_needs_owner, §2.3)
POST /projects/{owner}/{slug}/requests/{id}/withdraw the asker or requests:answer; a cross-owner reports_to request also the edge's two owners, and of the agents only the asker (§6.2, §2.3); an owner who is not a member withdraws under /requests/{id}/withdraw instead; a personal request is not_found here for every caller (below) {"reason"?}; status withdrawn; the card's status is restored

Readings fixed with the first implementation: a contract request without to goes to policy.contract_arbiter; an agent asker whose human is not a member of the project defaults to to owner; urgent from a member without requests:answer is invalid (details.fields.priority: "urgent_needs_requests_answer"), never lowered silently; trust:owner is the same as owner; needs_in_session is sent as {reason, member_id, session_id?} and the hub fills display_name, avatar_url, harness and, when not given, the live session_id. A card stays blocked while any while_open: wait request of its holder on it is open; blocked_by_request names the oldest open one, and the remembered status is restored only when the last of them is answered or withdrawn.

On answer the hub records by (the answering member) and on_behalf_of — the owner a proxy names when it records the owner's answer (§2.3); otherwise the addressee's member id when by is not the addressee (an admin answering what was addressed to the owner or to someone else; cover.for when the covering member answers), null when the addressee answered in its own right or the request was addressed to a role or trust token (there is no single addressee; by says who answered), so who answered on whose behalf is always visible — sets answered_at, clears blocked_by_request on the card and restores the status the card had before the block (in_progress, or review when it was asked after a report), and emits request_answered to the asking instance (readable by its whole agent) and, when set, the on_behalf_of member. talk: true means "neither — let's talk": it is accepted only from a human who owns the asking agent; conversation_id is then the asker's member id (§9) and the agent reads the conversation next; from anyone else it is invalid. A text beside talk is the human's note on it ("let's talk, about the second point"): it is stored as answer.text and travels with request_answered like any answer's text; the hub posts nothing into the conversation for it. A text beside reply_id is the same kind of note on the chosen reply.

The inbox. For every open request it shows one free-text field above the reply buttons. Each reply button and the talk button sends the field's text along when it holds any ({reply_id, text}, {talk: true, text}). A request without replies offers the field with a send button ({text}) beside talk. The talk button is labelled "Talk" in every language; it is offered only where talk is accepted (above).

Personal requests. Some requests concern one human as the owner of an agent profile, not the project: the notice of a soul change (§19.1), the notice of or consent to a change of another owner's project texts (§6.28), the notice of a cross-owner takeover (§6.19), the notice of the agent's outage (§15.7), of its unconfirmed Keep the Why commitment (§6.23) and of its isolation in a project (§6.2). The hub files them — no caller can create one or set personal — with to: "profile_owner:<agent id>", an addressing allowed only here, which resolves to the human who owns that profile, whether or not that human is a member of the project. A proposal for a human's preferences (§19.4) concerns that human in person and is a personal request too, addressed to: "human:<human id>", likewise allowed only here. So are the requests an organisation files (§6.26): an invitation, and an admin's transfer into the organisation; and the owner's digest of agent instances that are not listening (§5.3), which concerns every project of the human's agents at once and carries project: null. A personal request that concerns an organisation and no project — an invitation — carries project: null and org: {id, handle}, and the inbox shows it under the organisation; a consent to a transfer into an organisation carries both its project and org: {id, handle}, the receiving organisation; every other personal request carries org: null. Below, "the owner" of a personal request is the human it is addressed to. A personal request is in the owner's personal inbox, not in the project's request list:

Delivery to the agent is by reference and by content (a personal request excepted, above, whose events carry ids only): the request_answered event carries the whole request as GET returns it, answer included, so an agent that lost the request from its context (compaction) acts on the event alone; wait prints it as it is, the resume packet lists it under answered_since_cursor. Nothing is re-typed by a relay.

Forwarding and claiming a decision request. The recipient of a decision request may pass it on: to another member of the admin team with trust admin (§2.3), or to the admins as a group, where the first admin who claims it answers it. The jour fixe (§20.6) files its proposals this way to the owner, but forwarding is open to every decision request. A request with action goes only to a human with trust owner; a forward to the admins, to an agent or to anyone else is invalid (details.fields.to: "action_needs_owner").

A request always has exactly one current holder. It starts as to (a member, a role or trust token, or owner). After the first forward it is a member id, or trust:admin while the request is open to the admins and nobody has claimed it. When the holder leaves the project, the request goes back to the admins (holder: trust:admin). A request of an owner's round (§20.7) and a proxy's call (§2.3) go back to their addressee instead; one of an owner's round is withdrawn when its addressee has left too.

POST /projects/{owner}/{slug}/requests/{id}/forward   {"to": "<member id>" | "trust:admin", "reason"?: "…"}
POST /projects/{owner}/{slug}/requests/{id}/claim

An agent's answer on such a request is forbidden (humans_only). The §2.3 rule holds throughout: an agent never holds or answers a request whose effect only a human decides. - Answering. Once a request has been forwarded, only its current holder answers it. Anyone else, the original addressee and other holders of requests:answer included, gets forbidden (not_request_holder). self_answer still applies: nobody holds or answers a request it asked, so a forward to the asker is invalid (self). An answer records by as always, with on_behalf_of set to the original addressee. - Recorded on the request. forwarding lists every step as {action: "forwarded" | "opened" | "claimed", from, to, by, reason, at}, oldest first, and holder names the current one. GET …/requests?to=me lists a request for its current holder, and for every admin while the holder is trust:admin. It no longer lists it for an earlier holder. - Events. request_forwarded and request_claimed (§8.2) go to the asker, the old and the new holder, and the original addressee. A forward to a member wakes it by its agent's rule (§6.18). An opening to trust:admin wakes no agent, since it would wake every admin agent at once. Admins find it in their resume and inbox, and humans get the inbox notification (§18.1). A claim wakes nobody. - Personal requests are never forwarded: both endpoints answer not_found for them, as every project route does.