Skip to content

episodes.detach

Close an execution episode deliberately: the work is done, not merely interrupted, and the repository's record says so. Without 'external_id' it is whichever episode the calling connection holds, which needs an MCP session: over plain HTTP there is no connection, so the call is refused and the episode must be named. A client that simply goes away does not need this — its episode detaches and the reaper closes it as interrupted — and the difference between those two records is the one thing about an ended episode that changes what somebody does next.

Close an execution episode deliberately: the work is done, not merely interrupted, and the repository's record says so. Without 'external_id' it is whichever episode the calling connection holds, which needs an MCP session: over plain HTTP there is no connection, so the call is refused and the episode must be named. A client that simply goes away does not need this — its episode detaches and the reaper closes it as interrupted — and the difference between those two records is the one thing about an ended episode that changes what somebody does next.

  • 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_detach
HTTPPOST /api/v1/episodes/detach operationId episodes.detach
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 · DetachInput

The input of episodes.detach: the episode to close.

Empty is a valid input, meaning the episode the calling connection holds; over an interface with no connection that input is refused, since there is nothing to take "this one's episode" from.

use majordomus_cli::capability::builtin::episodes::DetachInput;
let mine: DetachInput = serde_json::from_str("{}").unwrap();
assert!(mine.external_id.is_none());
let named: DetachInput = serde_json::from_str(r#"{"external_id":"conv-42"}"#).unwrap();
assert_eq!(named.external_id.as_deref(), Some("conv-42"));
assert_eq!(serde_json::to_string(&mine).unwrap(), "{}", "an omitted id is not written as null");
propertytyperequireddescription
external_id string | null no The episode to close. Omitted, it is whichever one this connection holds, which is what a client ending its own sitting means.
JSON Schema
{
  "additionalProperties": false,
  "description": "The input of `episodes.detach`: the episode to close.\n\nEmpty is a valid input, meaning the episode the calling connection holds; over an\ninterface with no connection that input is refused, since there is nothing to take\n\"this one's episode\" from.\n\n```\nuse majordomus_cli::capability::builtin::episodes::DetachInput;\nlet mine: DetachInput = serde_json::from_str(\"{}\").unwrap();\nassert!(mine.external_id.is_none());\nlet named: DetachInput = serde_json::from_str(r#\"{\"external_id\":\"conv-42\"}\"#).unwrap();\nassert_eq!(named.external_id.as_deref(), Some(\"conv-42\"));\nassert_eq!(serde_json::to_string(&mine).unwrap(), \"{}\", \"an omitted id is not written as null\");\n```",
  "properties": {
    "external_id": {
      "description": "The episode to close. Omitted, it is whichever one this connection holds, which is\nwhat a client ending its own sitting means.",
      "type": [
        "string",
        "null"
      ]
    }
  },
  "title": "DetachInput",
  "type": "object"
}

output · Detached

The answer of episodes.detach: the episode as it was closed, no longer held by any connection, with what the repository's end event reported.

use majordomus_cli::capability::builtin::episodes::Detached;
let schema = serde_json::to_value(schemars::schema_for!(Detached)).unwrap();
assert_eq!(schema["required"], serde_json::json!(["episode"]));
propertytyperequireddescription
episode reference yes The episode as it was closed, its repository field carrying what the repository's own end event reported.
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.detach`: the episode as it was closed, no longer held by any\nconnection, with what the repository's end event reported.\n\n```\nuse majordomus_cli::capability::builtin::episodes::Detached;\nlet schema = serde_json::to_value(schemars::schema_for!(Detached)).unwrap();\nassert_eq!(schema[\"required\"], serde_json::json!([\"episode\"]));\n```",
  "properties": {
    "episode": {
      "$ref": "#/$defs/ConnectionEpisode",
      "description": "The episode as it was closed, its `repository` field carrying what the repository's\nown end event reported."
    }
  },
  "required": [
    "episode"
  ],
  "title": "Detached",
  "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.