Gateway Protocol And State Contracts¶
This page explains the current v1 gateway contracts: how attachability is published, what the live HTTP surface looks like, and which files under gateway/ are durable versus ephemeral.
For the broader runtime-root and session-root filesystem map around this subtree, use Agents And Runtime.
Mental Model¶
The gateway contract has two layers.
- Stable attachability tells the runtime how a session could gain a gateway.
- Live gateway bindings describe one currently running gateway instance.
Those layers are kept separate so a session can stay gateway-capable even when no sidecar is running.
Stable Attachability¶
Stable attachability is published through the manifest-first contract:
- tmux discovery env:
HOUMAO_MANIFEST_PATHHOUMAO_AGENT_ID- runtime-owned manifest authority:
<session-root>/manifest.json- derived outward-facing gateway bookkeeping:
<session-root>/gateway/gateway_manifest.json- internal bootstrap artifacts may also exist:
<session-root>/gateway/attach.json
The supported external contract for attach, resume, and relaunch is manifest.json together with tmux-local discovery and shared-registry fallback. gateway_manifest.json remains derived publication only.
attach.json may still exist as internal bootstrap state for gateway startup, offline status materialization, and metadata transfer. It is not the supported public attach authority.
Representative legacy/internal bootstrap payload for an old cao_rest session:
{
"schema_version": 1,
"attach_identity": "cao-rest-1",
"backend": "cao_rest",
"tmux_session_name": "HOUMAO-gpu",
"working_directory": "/abs/path/repo",
"backend_metadata": {
"api_base_url": "http://localhost:9889",
"terminal_id": "term-123",
"profile_name": "runtime-profile",
"profile_path": "/abs/path/runtime-profile.md",
"parsing_mode": "shadow_only"
},
"manifest_path": "/abs/path/.houmao/runtime/sessions/cao_rest/cao-rest-1/manifest.json",
"agent_def_dir": "/abs/path/tests/fixtures/plain-agent-def",
"runtime_session_id": "cao-rest-1",
"desired_host": "127.0.0.1",
"desired_port": 43123
}
Representative legacy/internal houmao_server_rest bootstrap payload:
{
"schema_version": 1,
"attach_identity": "cao-gpu",
"backend": "houmao_server_rest",
"tmux_session_name": "cao-gpu",
"working_directory": "/abs/path/repo",
"backend_metadata": {
"api_base_url": "http://127.0.0.1:9889",
"session_name": "cao-gpu",
"terminal_id": "term-123",
"parsing_mode": "shadow_only"
},
"manifest_path": "/abs/path/.houmao/runtime/sessions/houmao_server_rest/cao-gpu/manifest.json",
"agent_def_dir": "/abs/path/repo/.houmao/agents",
"runtime_session_id": "cao-gpu",
"desired_host": "127.0.0.1",
"desired_port": 43123
}
Current v1 scope:
- Runtime-owned tmux-backed sessions publish gateway capability.
- Live attach and request execution currently support runtime-owned
local_interactivesessions and runtime-owned native headless sessions (claude_headless,codex_headless,gemini_headless). Legacy REST-backed manifest records may still be recognized for old-artifact inspection or explicit rejection, but new public launches do not create them. - Gateway-owned live TUI tracking routes currently support attached runtime-owned
local_interactivesessions. Forlocal_interactive, the gateway derives tracked identity from durable internal bootstrap metadata plus manifest-backed authority and uses the runtime session id as the publicterminal_idcompatibility value because no backend-provided terminal alias exists on that path. - Native headless internal bootstrap metadata may also carry
managed_api_base_urlandmanaged_agent_reftogether when the live gateway should route requests back throughhoumao-passive-serverfor a passive-server-managed headless agent instead of resuming that headless session locally. attach.jsonmay keepmanifest_pathfor gateway internals, but the runtime-owned session manifest remains the supported persisted mailbox-capability contract for gateway mailbox routes and mail notifier support.gateway_manifest.jsonis derived publication only. It may expose desired listener data andgateway_pid, but attach and control behavior must trustmanifest.jsonplus tmux or registry discovery instead of treatinggateway_manifest.jsonas primary authority.
Current-session attach rules:
- tmux-published
HOUMAO_MANIFEST_PATHis the preferred current-session manifest locator - when
HOUMAO_MANIFEST_PATHis missing or stale,HOUMAO_AGENT_IDplus the shared registry must resolve exactly one freshruntime.manifest_path - the resolved manifest must belong to the current tmux session
- the resolved manifest must describe a maintained tmux-backed managed session
- manifest-declared authority and shared-registry identity are authoritative for current-session attach
- stable artifacts may publish before passive-server discovery catches up, so current-session attach readiness can be later than capability publication
Live Gateway Bindings¶
Live bindings exist only while a gateway process is running.
Published tmux env vars:
HOUMAO_AGENT_GATEWAY_HOSTHOUMAO_AGENT_GATEWAY_PORTHOUMAO_GATEWAY_STATE_PATHHOUMAO_GATEWAY_PROTOCOL_VERSION
Important rules:
- The runtime validates these bindings structurally before trusting them.
GET /healthis the authoritative liveness check for the live gateway.- A dead gateway can leave stale env behind temporarily; validation plus health probing is what cleans that up.
- These env vars are a runtime publication surface, not the preferred attached-mail discovery contract for agent turns. For shared-mailbox work, the supported runtime-owned resolver is
pixi run houmao-mgr agents self mail resolve-liveorpixi run houmao-mgr agents single --agent-name <name> mail resolve-live.
Gateway Client Proxy Policy¶
Gateway client calls are local control-plane HTTP requests to the live per-agent gateway listener. By default, GatewayClient connects directly to the resolved gateway listener and bypasses ambient proxy variables such as HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and their lowercase variants. This applies to health checks, status, request submission, prompt and TUI control, reminders, mail, mail-notifier, and memory calls made through the shared gateway client.
Set HOUMAO_GATEWAY_RESPECT_PROXY_ENV=1 in the process that constructs GatewayClient only when you intentionally want those live gateway calls to use normal Python environment proxy handling. In that mode, caller-provided proxy variables and NO_PROXY or no_proxy values are respected by the underlying HTTP client.
This gateway-specific switch is separate from the CAO loopback behavior that injects or preserves NO_PROXY entries. Gateway proxy bypass does not mutate process-wide NO_PROXY or no_proxy; it selects the gateway HTTP opener policy at client construction time.
HTTP Surface¶
Current v1 routes:
GET /healthGET /v1/statusPOST /v1/control/promptGET /v1/control/tui/stateGET /v1/control/tui/historyPOST /v1/control/tui/note-promptPOST /v1/control/send-keysGET /v1/control/headless/statePOST /v1/requestsPOST /v1/remindersGET /v1/remindersGET /v1/reminders/{reminder_id}PUT /v1/reminders/{reminder_id}DELETE /v1/reminders/{reminder_id}GET /v1/mail/statusPOST /v1/mail/listPOST /v1/mail/peekPOST /v1/mail/readPOST /v1/mail/sendPOST /v1/mail/postPOST /v1/mail/replyPOST /v1/mail/markPOST /v1/mail/movePOST /v1/mail/archiveGET /v1/mail-notifierPUT /v1/mail-notifierDELETE /v1/mail-notifier
GET /health¶
Gateway-local liveness only:
This does not mean the managed agent is available. It only means the gateway control plane is alive enough to serve its contract.
GET /v1/status¶
Status is shared by the live HTTP route and state.json.
Representative live status:
{
"schema_version": 1,
"protocol_version": "v1",
"attach_identity": "cao-rest-1",
"backend": "cao_rest",
"tmux_session_name": "HOUMAO-gpu",
"gateway_health": "healthy",
"managed_agent_connectivity": "connected",
"managed_agent_recovery": "idle",
"request_admission": "open",
"terminal_surface_eligibility": "ready",
"active_execution": "idle",
"execution_mode": "tmux_auxiliary_window",
"queue_depth": 0,
"gateway_host": "127.0.0.1",
"gateway_port": 43123,
"gateway_tmux_window_id": "@9",
"gateway_tmux_window_index": "2",
"gateway_tmux_pane_id": "%9",
"managed_agent_instance_epoch": 1,
"managed_agent_instance_id": "term-123"
}
Representative seeded offline status:
{
"schema_version": 1,
"protocol_version": "v1",
"attach_identity": "cao-rest-1",
"backend": "cao_rest",
"tmux_session_name": "HOUMAO-gpu",
"gateway_health": "not_attached",
"managed_agent_connectivity": "unavailable",
"managed_agent_recovery": "idle",
"request_admission": "blocked_unavailable",
"terminal_surface_eligibility": "unknown",
"active_execution": "idle",
"execution_mode": "detached_process",
"queue_depth": 0,
"managed_agent_instance_epoch": 0
}
Current status axes:
gateway_health:healthyornot_attachedmanaged_agent_connectivity:connectedorunavailablemanaged_agent_recovery:idle,awaiting_rebind, orreconciliation_requiredrequest_admission:open,blocked_unavailable, orblocked_reconciliationterminal_surface_eligibility:ready,unknown, ornot_readyactive_execution:idleorrunningexecution_mode:detached_processortmux_auxiliary_windowgateway_tmux_window_idandgateway_tmux_window_index: present for livetmux_auxiliary_windowstatus;gateway_tmux_window_indexmust never be"0"- seeded offline status carries the resolved desired execution mode even when no live gateway is attached
POST /v1/requests¶
Request Lifecycle¶
sequenceDiagram
participant C as Client
participant GW as Gateway<br/>Service
participant Q as SQLite<br/>Queue
participant W as Worker<br/>Loop
participant BE as Backend<br/>Session
C->>GW: POST /v1/requests
GW->>GW: admission check
GW->>Q: INSERT state=accepted
GW-->>C: 202 Accepted<br/>(request_id, queue_depth)
W->>Q: take next effective accepted
opt adjacent pending control intents
W->>Q: mark superseded records<br/>state=coalesced
end
Q-->>W: request record
W->>Q: UPDATE state=running
W->>BE: submit_prompt() or interrupt()
BE-->>W: result
W->>Q: UPDATE state=completed
Current public request kinds:
submit_promptinterrupt
The reminder timer path does not add a new public request kind. Reminders are registered and inspected only through /v1/reminders, and due reminders execute as gateway-owned in-memory behavior instead of becoming another public POST /v1/requests kind.
The notifier reminder path also does not add a new public request kind. The gateway may enqueue an internal mail_notifier_prompt record in queue.sqlite, but callers still control notifier behavior only through the dedicated /v1/mail-notifier routes.
POST /v1/requests stays the semantic queued prompt surface. For headless targets, both this route and POST /v1/control/prompt also accept an optional request-scoped execution.model object with normalized name plus optional reasoning.level as a tool/model-specific preset index. Higher unused numbers saturate to the highest maintained Houmao preset for the resolved ladder, and 0 means explicit off only when that ladder supports it. For immediate "send now or refuse now" prompt control, use POST /v1/control/prompt. For raw terminal mutation that must preserve exact <[key-name]> send-keys behavior without creating managed prompt history, use POST /v1/control/send-keys instead.
Before the worker promotes accepted durable work to running, it coalesces adjacent accepted control-intent records for the same managed-agent epoch. Coalescible records are limited to interrupt and submit_prompt records whose entire trimmed prompt is exactly /compact, /clear, or /new. Ordinary prompts, internal mail_notifier_prompt rows, unsupported request kinds, and different epoch values are hard boundaries. Duplicate interrupts collapse to one interrupt. Duplicate or superseded context-control prompts collapse to one final context action, with /new dominating /clear and /compact, and /clear dominating /compact. When a control run contains both interrupt and context action, one interrupt executes before the final context action. Superseded rows are retained as terminal coalesced records with result metadata and a coalesced gateway event; they do not contribute to active queue depth.
Representative prompt submission:
{
"schema_version": 1,
"kind": "submit_prompt",
"payload": {
"prompt": "hello",
"execution": {
"model": {
"name": "claude-3-7-sonnet",
"reasoning": {
"level": 7
}
}
}
}
}
Representative accepted response:
{
"request_id": "gwreq-20260313-000000Z-deadbeef",
"request_kind": "submit_prompt",
"state": "accepted",
"accepted_at_utc": "2026-03-13T00:00:00+00:00",
"queue_depth": 1,
"managed_agent_instance_epoch": 1
}
Observable current error semantics:
- malformed request payloads return HTTP
422from FastAPI validation, - TUI-backed prompt targets reject
execution.modelwith HTTP422instead of silently ignoring it, - reconciliation-blocked admission returns HTTP
409, - unavailable managed-agent admission returns HTTP
503.
The broader design leaves room for more policy-driven rejection states, but the current implementation should be documented as it exists today.
/v1/reminders¶
This route family manages direct gateway-owned reminders without going through the durable request queue.
Supported routes:
POST /v1/remindersGET /v1/remindersGET /v1/reminders/{reminder_id}PUT /v1/reminders/{reminder_id}DELETE /v1/reminders/{reminder_id}
Reminders are process-local in-memory state:
- pending reminders are lost when the gateway stops or restarts
- due-but-not-yet-delivered reminders are also lost on restart
- reminders do not create rows in
queue.sqliteuntil or unless some other gateway feature persists its own internal work GET /v1/remindersreports only the current live gateway process state- this is the direct live gateway HTTP surface underneath the supported scoped
houmao-mgr agents single/self ... gateway reminders ...CLI family and/houmao/agents/{agent_ref}/gateway/remindersprojection
Representative create request:
{
"schema_version": 1,
"reminders": [
{
"mode": "repeat",
"title": "Review inbox",
"prompt": "Review the inbox again.",
"ranking": -10,
"paused": false,
"start_after_seconds": 300,
"interval_seconds": 300
}
]
}
Representative send-keys reminder request:
{
"schema_version": 1,
"reminders": [
{
"mode": "one_off",
"title": "Dismiss dialog",
"send_keys": {
"sequence": "<[Escape]>",
"ensure_enter": false
},
"ranking": -100,
"paused": false,
"start_after_seconds": 5
}
]
}
Representative create response:
{
"schema_version": 1,
"effective_reminder_id": "greminder-deadbeefcafe",
"reminders": [
{
"schema_version": 1,
"reminder_id": "greminder-deadbeefcafe",
"mode": "repeat",
"delivery_kind": "prompt",
"title": "Review inbox",
"prompt": "Review the inbox again.",
"send_keys": null,
"ranking": -10,
"paused": false,
"selection_state": "effective",
"delivery_state": "scheduled",
"created_at_utc": "2026-03-31T00:00:00+00:00",
"next_due_at_utc": "2026-03-31T00:05:00+00:00",
"interval_seconds": 300.0,
"last_started_at_utc": null,
"blocked_by_reminder_id": null
}
]
}
Current behavior:
- reminders support
one_offandrepeatmodes - callers set
title,ranking, optionalpaused, exactly one ofpromptorsend_keys, and exactly one ofstart_after_secondsordeliver_at_utc send_keysuses{ "sequence": "...", "ensure_enter": true };ensure_enter=trueensures one trailing<[Enter]>, whileensure_enter=falsepreserves the exact sequence- reminder send-keys intentionally do not expose
escape_special_keys - smaller
rankingvalues win; rankings are signed integers and may be negative - equal rankings break deterministically by reminder creation order and then
reminder_id - only the effective reminder can dispatch; blocked reminders stay pending even if they are already due
- a paused effective reminder still blocks lower-priority reminders and sends no reminder delivery until it is updated or deleted
- repeating reminders require
interval_seconds - due effective reminders run only when
request_admission=open,active_execution=idle, and durable queue depth is zero - due prompt reminders submit semantic prompt text; due send-keys reminders submit raw control input through the same exact
<[key-name]>grammar asPOST /v1/control/send-keys - send-keys reminders do not submit
titleor anyprompttext when they fire - rest-backed and server-managed headless gateway targets reject send-keys reminders with HTTP
422at create or update time - when the gateway is busy, due effective reminders stay pending and show
delivery_state = "overdue" PUT /v1/reminders/{reminder_id}recomputes the effective reminder immediately after the update- repeating reminders keep anchored cadence and do not backfill missed intervals as an immediate burst
- deleting a scheduled or overdue reminder removes it immediately
- deleting an executing reminder only stops future occurrences; the already-started reminder delivery continues until completion
- unknown reminder ids return HTTP
404
POST /v1/control/prompt¶
This route is the direct prompt-control surface for gateway-managed sessions. It returns success only after the prompt has been admitted for immediate live dispatch on the current target, and it refuses by default when the target is not prompt-ready.
Representative request:
{
"schema_version": 1,
"prompt": "hello",
"force": false,
"execution": {
"model": {
"name": "claude-3-7-sonnet"
}
}
}
Headless prompt control also accepts an optional structured chat-session selector:
{
"schema_version": 1,
"prompt": "hello",
"force": false,
"chat_session": {
"mode": "tool_last_or_new"
}
}
Representative success response:
{
"status": "ok",
"action": "submit_prompt",
"sent": true,
"forced": false,
"detail": "Prompt dispatched."
}
Representative refusal payload (returned under an HTTP error status):
{
"detail": {
"status": "error",
"action": "submit_prompt",
"sent": false,
"forced": false,
"error_code": "not_ready",
"detail": "Gateway prompt rejected because the TUI is not submit-ready."
}
}
Current behavior:
- TUI-backed sessions require gateway-owned tracked TUI state to report a stable ready posture before prompt dispatch unless
force=true - Recoverable degraded chat context and current-error diagnostics do not by themselves block ordinary prompt dispatch when the TUI-backed session otherwise satisfies the prompt-ready contract
- TUI-backed sessions accept
chat_session.mode = "new"as a reset-then-send workflow that submits the tool-appropriate context-reset signal, waits for the tracked TUI surface to stabilize back to prompt-ready, and only then sends the caller prompt - Codex TUI reset-then-send uses
/new; other TUI tools use their configured reset signal, commonly/clear - TUI-backed sessions reject explicit
chat_session.mode = "auto" | "current" | "tool_last_or_new" | "exact"with HTTP422 - TUI-backed sessions also reject any
execution.modeloverride with HTTP422 - native local headless sessions require no active gateway-managed execution and no queued gateway work before prompt dispatch unless
force=true - native headless sessions accept
chat_session.mode = "auto" | "new" | "current" | "tool_last_or_new" | "exact";chat_session.idis required only formode = "exact" - native headless sessions accept optional
execution.model.nameplus optionalexecution.model.reasoning.level; the effective value merges with launch-resolved defaults for the current turn only and does not persist after the prompt completes - omitted headless
chat_sessionmeansmode = "auto", which resolves in order as pendingnext_prompt_override, pinnedcurrent, persistedstartup_default, then freshnew - recoverable degraded chat context does not force headless
chat_session.mode = "new"; ordinary headless selector resolution still applies unless the caller explicitly requests fresh context chat_session.mode = "current"fails explicitly when the managed session has no pinned current provider session- passive-server-managed native headless sessions reuse the managed-agent
can_accept_prompt_nowposture and reject overlapping work unlessforce=true force=truebypasses only readiness/busy posture; it does not bypass blank prompt validation, detached state, reconciliation blocking, or unsupported backendscodex_app_serverdirect gateway prompt control is not implemented- successful direct prompt control records gateway-owned prompt-note evidence for TUI-backed sessions
GET /v1/control/tui/state¶
This route returns the gateway-owned live HoumaoTerminalStateResponse for one attached TUI-backed session.
Current availability rules:
- attached runtime-owned
local_interactivesessions, and - HTTP
422for attached backends that do not have a gateway-owned TUI tracker.
For attached local_interactive, the gateway synthesizes tracked identity from internal bootstrap runtime_session_id metadata (falling back to attach_identity), keeps terminal_aliases empty, and therefore exposes the runtime session id as the public terminal_id on this route.
For attached runtime-owned local_interactive sessions outside passive-server, repo-owned local workflow guidance now centers on this route together with POST /v1/control/tui/note-prompt. That pairing is the supported local inspection and explicit-input-provenance surface.
GET /v1/control/tui/history¶
This route returns the gateway-owned live HoumaoTerminalSnapshotHistoryResponse for the same tracked TUI session.
It is a bounded in-memory recent snapshot surface rather than the coarse transition-summary history attached to HoumaoTerminalStateResponse.recent_transitions. The limit query parameter defaults to 100. Attached local_interactive sessions use the same tracked-session identity and terminal_id fallback behavior as GET /v1/control/tui/state.
The tracker retains at most 1000 recent snapshots per tracked session in memory. That retention cap is internal implementation configuration and is not currently a user-facing knob.
For attached runtime-owned local_interactive sessions outside passive-server, this route is now part of the supported local inspection workflow together with GET /v1/control/tui/state and POST /v1/control/tui/note-prompt.
POST /v1/control/tui/note-prompt¶
This route records explicit-input evidence on the gateway-owned tracker for the attached session and returns the updated HoumaoTerminalStateResponse.
It accepts the same payload shape as prompt submission (GatewayRequestPayloadSubmitPromptV1), but only the prompt value is consumed by the tracker. Successful submit_prompt execution through POST /v1/requests already records this prompt note automatically, so callers only need this route when they must preserve explicit-input provenance without routing the prompt through the gateway request queue.
POST /v1/control/send-keys¶
This route is the dedicated raw control-input surface for gateway-managed sessions. It bypasses the durable prompt queue and therefore does not claim that a managed prompt turn was submitted.
Representative request:
Representative success response:
{
"status": "ok",
"action": "control_input",
"detail": "Delivered control input to the local interactive session."
}
Current behavior:
- the route accepts the same exact
<[key-name]>grammar as runtimesend-keys, including optional whole-string literal escaping withescape_special_keys=true - the route does not enqueue a
submit_promptrequest inqueue.sqlite - the route does not create gateway-owned prompt-tracking notes by itself
- semantic prompt submission remains separate on
POST /v1/control/promptfor immediate control orPOST /v1/requestsfor queued execution, whilePOST /v1/control/send-keysremains the operator/debug raw-control path - REST-backed and server-managed headless gateway targets currently reject this route with HTTP
422because they do not preserve exact tmux key semantics on that path
GET /v1/control/headless/state¶
This route returns the read-optimized GatewayHeadlessControlStateV1 for attached native headless backends.
local_interactive sessions do not use this route. When attached, they expose gateway-owned live TUI state through /v1/control/tui/* instead.
The headless control-state payload includes chat_session with:
current: the concrete provider session id currently pinned by the managed session, ornullstartup_default: the first-chat fallback policy usingmode = "new" | "tool_last_or_new" | "exact"next_prompt_override: the one-shot live override consumed only by the next accepted direct prompt whose effective mode isauto
POST /v1/control/headless/next-prompt-session stores that one-shot override. In v1 it accepts only:
That override is live gateway state only. It is not persisted across restart, it is ignored by queued /v1/requests prompt execution, and it remains pending when later direct prompts explicitly request new, current, tool_last_or_new, or exact.
GET /v1/mail/status¶
This route reports whether the attached session exposes the shared gateway mailbox facade and which transport-backed binding it is using.
Representative response:
{
"schema_version": 1,
"transport": "filesystem",
"principal_id": "HOUMAO-gpu",
"address": "HOUMAO-gpu@agents.localhost",
"bindings_version": "2026-03-19T08:00:00.000001Z"
}
POST /v1/mail/list¶
This is the shared mailbox listing path for both filesystem-backed and stalwart-backed sessions. It returns message metadata and may include full message body content when explicitly requested.
Representative request:
{
"schema_version": 1,
"box": "inbox",
"read_state": "unread",
"answered_state": "any",
"archived": false,
"limit": 10,
"include_body": false
}
Representative response:
{
"schema_version": 1,
"transport": "filesystem",
"principal_id": "HOUMAO-gpu",
"address": "HOUMAO-gpu@agents.localhost",
"box": "inbox",
"message_count": 1,
"open_count": 1,
"unread_count": 1,
"messages": [
{
"message_ref": "filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011",
"thread_ref": "filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011",
"created_at_utc": "2026-03-19T08:00:00Z",
"subject": "Gateway unread reminder",
"unread": true,
"body_preview": "Hello from the shared mailbox surface",
"sender": {
"address": "HOUMAO-sender@agents.localhost"
},
"to": [
{
"address": "HOUMAO-gpu@agents.localhost"
}
],
"cc": [],
"reply_to": [],
"attachments": []
}
]
}
Shared mailbox reference rules:
message_refis the stable target for shared gateway mailbox message, reply, and lifecycle routes.thread_refis optional and opaque for callers.- Callers must not derive behavior from transport-specific prefixes embedded in those refs.
POST /v1/mail/peek¶
This route returns one selected shared mailbox message without changing read state.
Representative request:
{
"schema_version": 1,
"message_ref": "filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011"
}
POST /v1/mail/read¶
This route returns one selected shared mailbox message and marks it read for the current mailbox principal.
Representative request:
{
"schema_version": 1,
"message_ref": "filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011"
}
POST /v1/mail/send¶
This route sends a new shared mailbox message without consuming the terminal-mutation slot used by POST /v1/requests.
Representative request:
{
"schema_version": 1,
"to": ["HOUMAO-orchestrator@agents.localhost"],
"cc": [],
"subject": "Investigate parser drift",
"body_content": "Hello from the gateway facade",
"attachments": [],
"notify_block": {"text": "re-run on official path before reporting", "placement": "append"},
"notify_auth": {"scheme": "none"}
}
POST /v1/mail/post¶
This route posts an operator-origin note into the attached session's mailbox. In v1, operator-origin post is supported for filesystem mailbox bindings and rejected for Stalwart-backed bindings.
Representative request:
{
"schema_version": 1,
"subject": "Resume after sync",
"body_content": "Continue from the latest mailbox checkpoint.",
"reply_policy": "operator_mailbox",
"attachments": [],
"notify_block": {"text": "continue current task", "placement": "prepend"}
}
POST /v1/mail/reply¶
This route replies to an existing shared mailbox message using the opaque message_ref returned by list, peek, or read.
Representative request:
{
"schema_version": 1,
"message_ref": "filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011",
"body_content": "Reply with next steps",
"attachments": []
}
Optional notification-prompt fields¶
/v1/mail/send, /v1/mail/post, and /v1/mail/reply accept two optional canonical-envelope fields:
notify_block— typed sub-model{"text": str, "placement": "append" | "prepend"}carrying short sender-marked notification text (≤ 512 characters) plus placement metadata.placementdefaults to"append". Whennotify_blockis omitted, the gateway scansbody_contentfor the first```houmao-notifyfenced code block and extracts it intonotify_block.textat composition time withplacement="append". An explicitnotify_blockvalue bypasses body-fence extraction.- The protocol-side auto-mirror invariant synthesizes a
houmao-notifyfenced block inbody_markdownat the requested placement when the body does not already contain one, so the receiver always finds the same text in the message body even though the channel reaches the wake-up prompt earlier. notify_auth— sender-supplied authentication metadata. The protocol reservesschemevaluesnone,shared-token,hmac-sha256, andjws; in this protocol version onlyscheme="none"is accepted at canonical-envelope validation. Non-noneschemes are rejected with HTTP422and the canonicalverifier not yet supportedvalidation error. The gateway notifier consumesnotify_auththrough a pluggable verifier interface; see Mail-notifier trust posture for the configurable knobs.
Both fields are optional and default to absent. Stalwart-bound sends reject these fields with HTTP 422 until JMAP-side projection ships in a follow-on change. See docs/reference/mailbox/contracts/canonical-model.md for the canonical envelope semantics.
POST /v1/mail/mark¶
This route applies explicit mailbox lifecycle flags to selected messages.
Representative request:
{
"schema_version": 1,
"message_refs": ["filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011"],
"read": true,
"answered": true
}
POST /v1/mail/move¶
This route moves selected messages to another mailbox box.
{
"schema_version": 1,
"message_refs": ["filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011"],
"destination_box": "archive"
}
POST /v1/mail/archive¶
This route is the standard shortcut for archiving selected processed messages.
{
"schema_version": 1,
"message_refs": ["filesystem:msg-20260319T080000Z-a1b2c3d4e5f64798aabbccddeeff0011"]
}
Shared lifecycle rules:
message_refandmessage_refsare the full targeting contract; callers must not derive transport-local ids from them.readmarks one selected message read by returning the full message envelope, whilemark,move, andarchivereturn lifecycle responses containing the affected message envelopes.- Like the other shared mailbox routes, lifecycle routes do not consume the terminal-mutation slot behind
POST /v1/requests.
Shared mailbox route availability rules:
/v1/mail/*is available only when the live gateway listener is bound to127.0.0.1.- A gateway listener bound to
0.0.0.0rejects shared mailbox routes with HTTP503. - Sessions without a usable manifest-backed mailbox binding reject shared mailbox routes with HTTP
422. - Transport adapter failures return HTTP
502.
GET|PUT|DELETE /v1/mail-notifier¶
These routes manage the gateway-owned mail-notifier loop for mailbox-enabled sessions.
Representative enable request:
{
"schema_version": 1,
"enabled": true,
"interval_seconds": 60,
"mode": "any_inbox",
"appendix_text": "Prioritize release-blocking mail.",
"context_error_policy": "continue_current",
"pre_notification_context_action": "none"
}
Representative status response:
{
"schema_version": 1,
"enabled": true,
"interval_seconds": 60,
"mode": "any_inbox",
"appendix_text": "Prioritize release-blocking mail.",
"context_error_policy": "continue_current",
"pre_notification_context_action": "none",
"supported": true,
"support_error": null,
"last_poll_at_utc": "2026-03-16T09:45:00+00:00",
"last_notification_at_utc": "2026-03-16T09:45:00+00:00",
"last_error": null
}
Support contract rules:
- The gateway resolves the runtime-owned session manifest through internal bootstrap metadata, typically
attach.json.manifest_path. - It inspects
payload.launch_plan.mailboxin that manifest as the durable mailbox capability record. - It validates current mailbox actionability from that manifest-backed binding and transport-local prerequisites before treating notifier behavior as supported.
- The notifier wake-up prompt itself stays on the runtime-owned discovery contract for the agent turn: it points the agent at
resolve-live, which derives current mailbox fields from the durable binding and returns optionalgateway.base_urldata when a valid live gateway is attached. - Enabling the notifier fails explicitly when the internal bootstrap state cannot resolve a readable manifest, when the manifest launch plan has no mailbox binding, or when the current manifest-backed binding is not actionable for notifier work.
- Eligible inbox truth comes from the shared gateway mailbox facade rather than mailbox-local SQLite, while notifier cadence, readiness-gated reminder delivery, last-error bookkeeping, and durable per-poll notifier audit history remain gateway-owned state in
queue.sqlite. - The notifier mode selects the inbox filter:
any_inboxwakes for any unarchived inbox mail, including read or answered mail, whileunread_onlywakes only for unread unarchived inbox mail. appendix_textis optional runtime guidance appended to rendered notifier prompts.PUT /v1/mail-notifierpreserves the stored appendix when the field is omitted, replaces it when a non-empty string is supplied, and clears it whenappendix_textis"".context_error_policydefaults tocontinue_current.clear_contextis opt-in and only runs clean-context recovery when the current degraded diagnostic is recognized for the owning CLI tool.pre_notification_context_actiondefaults tonone.compactruns before every notification prompt only on supported tool/backend combinations; v1 supports Codex TUI via/compactand reports other combinations as unsupported.DELETE /v1/mail-notifierdisables polling without clearing storedappendix_text.- Notifier audit rows now persist shared
message_refandthread_refvalues instead of transport-local mailbox ids. - Wake-up prompts summarize the current eligible inbox snapshot and let the agent choose which message or messages to inspect and handle.
- Each reminder includes the eligible
message_ref, optionalthread_ref, sender context, subject, and creation timestamp for every selected message in that snapshot. - If eligible inbox mail remains unchanged after an earlier reminder, later prompt-ready polls may enqueue another reminder because reminder eligibility depends on current mailbox truth plus live prompt readiness rather than on reminder history.
- Recoverable degraded chat context does not by itself cause a busy skip and does not force a clean-context notifier prompt. If the target is otherwise prompt-ready and queue admission passes, the notifier uses normal current-context prompt work unless
context_error_policy=clear_contextmatches a recognized tool-owned compaction diagnostic. - The canonical envelope's
notify_blockandnotify_authfields are accepted on/v1/mail/send|post|reply, persisted with the stored message, and rendered into the gateway notifier wake-up prompt at the requestedplacementsubject to the configured trust posture (see Mail-notifier trust posture).
Detailed inspection note:
GET /v1/mail-notifierstays a compact snapshot surface and includes effectiveappendix_text,context_error_policy,pre_notification_context_action, plus the notify-block configuration (notify_block_render,notify_block_auth_mode,notify_block_auth_verifier,notify_block_shared_tokens,notify_block_per_message_chars,notify_block_total_chars).- Detailed per-poll decision history lives in the
gateway_notifier_audittable insidequeue.sqlite. The per-poll row includesrendered_block_entries_json, a JSON array of per-rendered-block audit entries (message_ref,rendered,auth_scheme,auth_outcome,auth_detail,block_chars,block_truncated).
Mail-notifier trust posture¶
The notifier configuration carries six fields that govern notify-block rendering:
| Field | Type | Default | Effect |
|---|---|---|---|
notify_block_render |
enabled | disabled |
enabled |
When disabled, every notify_block is suppressed from the rendered prompt regardless of verifier outcome; audit records rendered=false with detail "render disabled". |
notify_block_auth_mode |
permissive-log | required |
permissive-log |
permissive-log runs the verifier and records the outcome in audit but renders the block regardless. required suppresses rendering when the verifier reports passed=false. |
notify_block_auth_verifier |
none | shared-token |
none |
none selects the always-pass PermissiveVerifier. shared-token selects SharedTokenVerifier, which compares notify_auth.token against notify_block_shared_tokens; rejection details never echo the supplied token text. |
notify_block_shared_tokens |
list[str] |
[] |
Allowlist consulted by SharedTokenVerifier. With notify_block_auth_mode=required and an empty allowlist, every notify-block is suppressed. |
notify_block_per_message_chars |
int |
512 |
Per-rendered-block size cap. Oversized text is truncated to this length minus one plus a trailing …. |
notify_block_total_chars |
int |
2048 |
Aggregate cap across all rendered blocks per notifier prompt. When the cap fires, additional blocks are suppressed and a + N more sender notice(s) — open inbox to read summary line is emitted instead. |
PUT /v1/mail-notifier accepts these fields with the same omit-preserves-current behavior as appendix_text. GET /v1/mail-notifier surfaces the effective values in the status response.
Current-Instance Execution Handle¶
run/current-instance.json is the authoritative live execution record for one attached gateway instance.
Representative detached-process payload:
{
"schema_version": 1,
"protocol_version": "v1",
"pid": 424242,
"host": "127.0.0.1",
"port": 43123,
"execution_mode": "detached_process",
"managed_agent_instance_epoch": 1,
"managed_agent_instance_id": "term-123"
}
Representative same-session auxiliary-window payload:
{
"schema_version": 1,
"protocol_version": "v1",
"pid": 424242,
"host": "127.0.0.1",
"port": 43123,
"execution_mode": "tmux_auxiliary_window",
"tmux_window_id": "@2",
"tmux_window_index": "1",
"tmux_pane_id": "%7",
"managed_agent_instance_epoch": 1,
"managed_agent_instance_id": "term-123"
}
Current rules:
execution_mode = "detached_process"must omit tmux execution-handle fieldsexecution_mode = "tmux_auxiliary_window"must includetmux_window_id,tmux_window_index, andtmux_pane_id- same-session mode must never record
tmux_window_index = "0" - for same-session auxiliary-window mode, the recorded tmux handle is the authoritative live gateway surface for attach, detach, cleanup, and auxiliary-window recreation
- non-zero tmux windows remain non-contractual by convention; callers should rely on the recorded current-instance handle rather than window naming heuristics
- once the session root is known,
run/current-instance.jsonis also the authoritative local live-gateway record used by runtime-owned cross-session endpoint discovery
Durable And Ephemeral Gateway Artifacts¶
For the full runtime-managed session tree that surrounds gateway/, use Agents And Runtime. This page keeps the gateway-local artifact semantics.
Representative gateway tree:
<session-root>/gateway/
attach.json
gateway_manifest.json
protocol-version.txt
desired-config.json
state.json
queue.sqlite
events.jsonl
logs/
gateway.log
diagnostics/
gateway-diagnostic.log
run/
current-instance.json
gateway.pid
Artifact roles:
attach.json: internal bootstrap stategateway_manifest.json: derived outward-facing gateway bookkeepingprotocol-version.txt: simple version marker for local artifactsdesired-config.json: desired host, port, execution mode, TUI tracking timings, and optional diagnostic logging settings to reuse on later startsstate.json: read-optimized current status contractqueue.sqlite: durable queue records, the singleton gateway-owned mail notifier record, and thegateway_notifier_audittable that records one structured notifier decision row per enabled poll cycleevents.jsonl: append-only event loglogs/gateway.log: append-only line-oriented running log for lifecycle, notifier polling, busy deferrals, and execution outcomeslogs/diagnostics/gateway-diagnostic.log: opt-in structured JSONL diagnostic log. Rotated siblings use numeric suffixes such asgateway-diagnostic.log.1. These files are cleanup-sensitive log artifacts, not durable gateway state.run/current-instance.json: current process id, host, port, upstream epoch and instance id, plus same-session execution-handle fields when the gateway is hosted in a tmux auxiliary windowrun/gateway.pid: pidfile mirror; still written for same-session mode, but the tmux execution handle incurrent-instance.jsonis the authoritative stop or cleanup target
Operator note:
That log is the stable tail-watch surface for the running gateway. Request lifecycle history still lives in events.jsonl, while detailed mail-notifier decision history now lives in queue.sqlite.gateway_notifier_audit. gateway.log remains the human-oriented running log for day-to-day observation.
When desired-config.json includes desired_diagnostic_logging.enabled=true, the gateway also writes structured diagnostic lines to <session-root>/gateway/logs/diagnostics/gateway-diagnostic.log. Diagnostic entries cover HTTP completion status, request-body validation failures before route handlers run, mailbox facade operation start/success/failure, and selected queue/control/reminder/notifier warning or error paths. The diagnostic logger builds entries from explicit safe fields: it does not write mailbox bodies, raw prompt text, attachment contents, authorization headers, cookies, bearer tokens, credential material, or environment secrets by default. Consecutive warning/error diagnostics with the same semantic key are suppressed after the first entry and later summarized with a diagnostic.dedup_summary entry reporting suppressed_count.
Diagnostic logs are bounded by max_bytes and backup_count. They are useful postmortem evidence, but they do not replace durable contracts: queue.sqlite remains authoritative for queued request and notifier audit state, events.jsonl remains append-only gateway event history, state.json remains the status snapshot, and manifest files remain the durable session authority.
Current Implementation Notes¶
state.jsonexists even before the first live attach.- Offline status must omit live
gateway_hostandgateway_port. - The gateway client connects to
127.0.0.1even when the published host is0.0.0.0, because0.0.0.0is a bind address, not a connect address. - The session manifest remains stable authority and must not persist live gateway host or port.
- Shared-registry gateway metadata is locator metadata only; runtime-owned discovery recovers
runtime.manifest_path, derives the session root, and then validates the live endpoint againstrun/current-instance.jsonplus/health.