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.
| surface | as |
|---|---|
| MCP | tool majordomus_session_detach |
| HTTP | POST /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
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");| property | type | required | description |
|---|---|---|---|
| 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
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"]));| property | type | required | description |
|---|---|---|---|
| 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.