Skip to content

episodes.attach

Open an execution episode for the calling client, or resume the one it already had. 'external_id' is the client's own durable name for the sitting — its conversation or thread id — and must survive a reconnect: a client that comes back under the same identity is given its own episode again, under whatever peer id it now has, rather than a second one. This server's peer id will not do; it is handed out per connection. After this the client's own traffic keeps the episode alive, losing the connection detaches rather than closes it, and it is closed by majordomus_session_detach, by the server stopping, or by the reaper once the reattach grace has passed. The repository's episode is opened by the same command a provider hook runs, and what it reported is in the answer. Needs an MCP session: over plain HTTP there is no connection to bind to.

Open an execution episode for the calling client, or resume the one it already had. 'external_id' is the client's own durable name for the sitting — its conversation or thread id — and must survive a reconnect: a client that comes back under the same identity is given its own episode again, under whatever peer id it now has, rather than a second one. This server's peer id will not do; it is handed out per connection. After this the client's own traffic keeps the episode alive, losing the connection detaches rather than closes it, and it is closed by majordomus_session_detach, by the server stopping, or by the reaper once the reattach grace has passed. The repository's episode is opened by the same command a provider hook runs, and what it reported is in the answer. Needs an MCP session: over plain HTTP there is no connection to bind to.

  • command
  • behaviorally_verified
  • module episodes
  • #episodes
  • #session
  • #coordination

Exposure

Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.

surfaceas
MCPtool majordomus_session_attach
HTTPPOST /api/v1/episodes/attach operationId episodes.attach
command line—

Schemas

The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.

input · AttachInput

The input of episodes.attach: which piece of work this connection is carrying.

The identity is the only field, and an unknown one is refused rather than ignored, so a client that sends a peer id under another name learns it at once:

use majordomus_cli::capability::builtin::episodes::AttachInput;
let input: AttachInput = serde_json::from_str(r#"{"external_id":"conv-42"}"#).unwrap();
assert_eq!(input.external_id, "conv-42");
assert!(serde_json::from_str::<AttachInput>(r#"{"external_id":"x","peer":"p1"}"#).is_err());
assert!(serde_json::from_str::<AttachInput>("{}").is_err(), "the identity is required");
propertytyperequireddescription
external_id string yes The client's own durable name for this episode — its conversation id, thread id, or whatever it calls the sitting it is in.

It must survive a reconnect, because that is the whole of its job: a client that comes back under the same identity is given its own episode again rather than a second one. Anything the client can reproduce will do; what will not do is this server's peer id, which is handed out per connection and is a different string every time the client reconnects.

JSON Schema
{
  "additionalProperties": false,
  "description": "The input of `episodes.attach`: which piece of work this connection is carrying.\n\nThe identity is the only field, and an unknown one is refused rather than ignored, so a\nclient that sends a peer id under another name learns it at once:\n\n```\nuse majordomus_cli::capability::builtin::episodes::AttachInput;\nlet input: AttachInput = serde_json::from_str(r#\"{\"external_id\":\"conv-42\"}\"#).unwrap();\nassert_eq!(input.external_id, \"conv-42\");\nassert!(serde_json::from_str::<AttachInput>(r#\"{\"external_id\":\"x\",\"peer\":\"p1\"}\"#).is_err());\nassert!(serde_json::from_str::<AttachInput>(\"{}\").is_err(), \"the identity is required\");\n```",
  "properties": {
    "external_id": {
      "description": "The client's own durable name for this episode — its conversation id, thread id, or\nwhatever it calls the sitting it is in.\n\n**It must survive a reconnect**, because that is the whole of its job: a client that\ncomes back under the same identity is given its own episode again rather than a\nsecond one. Anything the client can reproduce will do; what will not do is this\nserver's peer id, which is handed out per connection and is a different string every\ntime the client reconnects.",
      "type": "string"
    }
  },
  "required": [
    "external_id"
  ],
  "title": "AttachInput",
  "type": "object"
}

output · Attached

The answer of episodes.attach: the episode the connection now holds, whether it was resumed rather than opened, and how long it outlives a lost connection.

use majordomus_cli::capability::builtin::episodes::Attached;
let schema = serde_json::to_value(schemars::schema_for!(Attached)).unwrap();
for field in ["episode", "resumed", "reattach_grace_seconds"] {
    assert!(schema["required"].as_array().unwrap().iter().any(|f| f == field), "{field}");
}
propertytyperequireddescription
episode reference yes The episode this connection now holds.
reattach_grace_seconds integer yes How long this episode survives its connection before the reaper closes it, in seconds. Stated rather than left to be discovered: a client that knows the grace can decide whether reconnecting is worth it.
resumed boolean yes Whether an episode already under this identity was resumed rather than a new one opened. True after a reconnect, a client restart or a crash, and the field a caller checks to know whether it is continuing something.
JSON Schema
{
  "$defs": {
    "ConnectionEpisode": {
      "description": "One episode: what the client calls itself, which connection holds it now, and what the\nrepository did about it.\n\n```\nuse majordomus_cli::episodes::{ConnectionEpisode, EpisodeBoard};\nuse majordomus_cli::peers::{PeerBoard, Transport};\nlet peer = PeerBoard::new().attach(Transport::Http);\nlet (episode, _) = EpisodeBoard::new().attach(&peer, \"conv-42\", \"generic\");\nlet episode: ConnectionEpisode = episode;\nassert_eq!((episode.external_id.as_str(), episode.provider.as_str()), (\"conv-42\", \"generic\"));\nassert_eq!(episode.peer.as_ref(), Some(&peer));\n// never empty: a board with no repository behind it says so\nassert!(episode.repository.contains(\"without a driver\"));\n```",
      "properties": {
        "attachments": {
          "description": "How many times a connection has taken it over, the first attach included.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "external_id": {
          "description": "The client's own durable name for this piece of work, and the key everything here is\naddressed by. Never a peer id: see the module documentation.",
          "type": "string"
        },
        "last_activity_seconds_ago": {
          "description": "Seconds since the client last spoke on it, through any connection.",
          "format": "uint64",
          "minimum": 0,
          "type": "integer"
        },
        "opened_at": {
          "description": "When it was first opened, RFC 3339, UTC. Unchanged by a re-attach: the episode is\nthe work, and the work did not restart because a socket did.",
          "type": "string"
        },
        "peer": {
          "anyOf": [
            {
              "$ref": "#/$defs/PeerId"
            },
            {
              "type": "null"
            }
          ],
          "description": "The connection holding it, when one does. `None` while detached — which is a state\nan episode is *supposed* to reach and come back from, not a fault."
        },
        "provider": {
          "description": "The provider id this episode is recorded under, as `share/providers.yaml` names one.",
          "type": "string"
        },
        "repository": {
          "description": "What the repository's own episode command reported, verbatim — the record it opened,\nor the reason it could not. Never interpreted here, and never empty: a subsystem\nthat silently does nothing is the failure mode this field exists to make visible.",
          "type": "string"
        },
        "state": {
          "$ref": "#/$defs/ConnectionEpisodeState",
          "description": "Open, or detached and waiting for its client."
        }
      },
      "required": [
        "external_id",
        "provider",
        "opened_at",
        "last_activity_seconds_ago",
        "attachments",
        "state",
        "repository"
      ],
      "type": "object"
    },
    "ConnectionEpisodeState": {
      "description": "Where an episode stands: held by a live connection, or waiting for its client to come\nback. There is no closed state, because a closed episode leaves the board; what survives\nit is the repository's own record.\n\n```\nuse majordomus_cli::episodes::{ConnectionEpisodeState, EpisodeBoard};\nuse majordomus_cli::peers::{PeerBoard, Transport};\nlet peer = PeerBoard::new().attach(Transport::Stdio);\nlet board = EpisodeBoard::new();\nassert_eq!(board.attach(&peer, \"w\", \"generic\").0.state, ConnectionEpisodeState::Open);\nassert_eq!(board.detach(&peer)[0].state, ConnectionEpisodeState::Detached);\nassert_eq!(serde_json::to_string(&ConnectionEpisodeState::Detached).unwrap(), \"\\\"detached\\\"\");\n```",
      "oneOf": [
        {
          "const": "open",
          "description": "A connection holds it and the client is speaking.",
          "type": "string"
        },
        {
          "const": "detached",
          "description": "The connection has gone; the episode is kept for `REATTACH_GRACE` so the client\ncan come back to it. This is the state a crash lands in, and the one a reconnect\nrecovers from.",
          "type": "string"
        }
      ]
    },
    "PeerId": {
      "description": "A peer's identity for the life of the server: `p1`, `p2`, ... in attachment order.\n`p1` is the session that started the server.",
      "type": "string"
    }
  },
  "description": "The answer of `episodes.attach`: the episode the connection now holds, whether it was\nresumed rather than opened, and how long it outlives a lost connection.\n\n```\nuse majordomus_cli::capability::builtin::episodes::Attached;\nlet schema = serde_json::to_value(schemars::schema_for!(Attached)).unwrap();\nfor field in [\"episode\", \"resumed\", \"reattach_grace_seconds\"] {\n    assert!(schema[\"required\"].as_array().unwrap().iter().any(|f| f == field), \"{field}\");\n}\n```",
  "properties": {
    "episode": {
      "$ref": "#/$defs/ConnectionEpisode",
      "description": "The episode this connection now holds."
    },
    "reattach_grace_seconds": {
      "description": "How long this episode survives its connection before the reaper closes it, in\nseconds. Stated rather than left to be discovered: a client that knows the grace can\ndecide whether reconnecting is worth it.",
      "format": "uint64",
      "minimum": 0,
      "type": "integer"
    },
    "resumed": {
      "description": "Whether an episode already under this identity was resumed rather than a new one\nopened. True after a reconnect, a client restart or a crash, and the field a caller\nchecks to know whether it is continuing something.",
      "type": "boolean"
    }
  },
  "required": [
    "episode",
    "resumed",
    "reattach_grace_seconds"
  ],
  "title": "Attached",
  "type": "object"
}

Policies

benchmark
required — a target on every transport the exposure declares; the cases are the input type's
cache
disabled — every call runs the handler

Benchmark targets

Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.

transportstatecasestargets
directcovered1default
mcpcovered1default
httpcovered1default

Searches the titles and descriptions of every page this site publishes.