1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//! The resource layer: pane handles and refs, re-resolved against the rows
//! `terminal show` and `terminal list` answer with.
use super::cli::is_stale;
use super::session::OrcaBackend;
use crate::backend::*;
/// Read one non-empty string field from a session's `backend_ref`.
pub(super) fn ref_str(session: &SessionRef, key: &str) -> Option<String> {
session
.backend_ref
.get(key)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_owned)
}
/// The stable per-tab keys Orca repeats in `terminal create` responses and
/// `terminal list` rows. `pane_key` is `tabId:leafId`: it survives a runtime
/// restart, while the `term_<uuid>` handle is minted per PTY incarnation.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct TabKeys {
pub(super) handle: Option<String>,
pub(super) pane_key: Option<String>,
tab_id: Option<String>,
leaf_id: Option<String>,
worktree_id: Option<String>,
pty_id: Option<String>,
}
impl TabKeys {
/// Read one CLI row. `create`/`show` nest the row under `terminal`,
/// `list` under `terminals`, so every accepted position is tried; a key
/// the build does not send stays `None`.
pub(super) fn read(row: &Value) -> Self {
let text = |key: &str| {
["", "/terminal", "/result/terminal"]
.iter()
.find_map(|prefix| row.pointer(&format!("{prefix}/{key}")))
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_owned)
};
let tab_id = text("tabId");
let leaf_id = text("leafId");
let pane_key = text("paneKey").or_else(|| match (&tab_id, &leaf_id) {
(Some(tab), Some(leaf)) => Some(format!("{tab}:{leaf}")),
_ => None,
});
Self {
handle: text("handle"),
pane_key,
tab_id,
leaf_id,
worktree_id: text("worktreeId"),
pty_id: text("ptyId"),
}
}
/// Read what a [`SessionRef`] already persisted.
pub(super) fn from_ref(session: &SessionRef) -> Self {
Self {
handle: ref_str(session, "handle"),
pane_key: ref_str(session, "pane_key"),
tab_id: ref_str(session, "tab_id"),
leaf_id: ref_str(session, "leaf_id"),
worktree_id: ref_str(session, "worktree_id"),
pty_id: ref_str(session, "pty_id"),
}
}
/// The keys worth persisting. Absent keys are omitted rather than null,
/// so a ref written before one of them existed still reads.
pub(super) fn to_ref(&self) -> Value {
let mut map = serde_json::Map::new();
for (key, value) in [
("handle", &self.handle),
("pane_key", &self.pane_key),
("tab_id", &self.tab_id),
("leaf_id", &self.leaf_id),
("worktree_id", &self.worktree_id),
("pty_id", &self.pty_id),
] {
if let Some(value) = value {
map.insert(key.to_string(), Value::String(value.clone()));
}
}
Value::Object(map)
}
}
impl OrcaBackend {
pub(super) fn ref_handle(session: &SessionRef) -> Result<String> {
ref_str(session, "handle")
.ok_or_else(|| anyhow::anyhow!("orca session ref missing string handle"))
}
/// Re-resolve a session's handle from its stable `pane_key`.
///
/// Orca's CLI has no paneKey→handle command, so the stable key round-trips
/// through `terminal list`: rows carry the pane key Orca persisted and the
/// handle of the pane's current PTY. Every other field of the ref is kept,
/// including the selector, and the observed keys are merged in.
fn remint(&self, session: &SessionRef) -> Result<SessionRef> {
let pane_key = ref_str(session, "pane_key").ok_or_else(|| {
anyhow::anyhow!("orca session ref has no pane_key; the handle cannot be re-resolved")
})?;
let selector = ref_str(session, "selector");
let row = self
.rows(selector.as_deref())?
.into_iter()
.find(|row| TabKeys::read(row).pane_key.as_deref() == Some(pane_key.as_str()))
.ok_or_else(|| {
CommandFailure::new(
format!("orca terminal list (pane {pane_key})"),
1,
Some("terminal_not_found".into()),
Some(format!("pane {pane_key} is not listed in any worktree")),
)
})?;
let keys = TabKeys::read(&row);
if keys.handle.is_none() {
anyhow::bail!("orca terminal list row for pane {pane_key} has no handle");
}
let mut backend_ref = session.backend_ref.clone();
if let (Some(map), Value::Object(observed)) = (backend_ref.as_object_mut(), keys.to_ref()) {
map.extend(observed);
}
Ok(SessionRef {
backend_ref,
..session.clone()
})
}
/// Where the tab a `terminal create` just made is, when its response named
/// no handle.
///
/// The coordinates the response did carry are an exact hook: `terminal list`
/// repeats `paneKey`, or the `tabId:leafId` pair it is built from, beside the
/// handle of the pane's current PTY. A response with no coordinates leaves
/// the create-time title, which Orca's login shell replaces within seconds,
/// so the newest row carrying that exact title wins. Position alone never
/// picks a tab: closing one this backend cannot identify is worse than
/// leaving it for the operator.
fn find_created(&self, keys: &TabKeys, title: &str) -> Option<String> {
let rows = self.rows(self.selector_for().as_deref()).ok()?;
if let Some(wanted) = keys.pane_key.as_deref() {
let named = rows
.iter()
.find(|row| TabKeys::read(row).pane_key.as_deref() == Some(wanted));
if let Some(row) = named {
return TabKeys::read(row).handle;
}
}
rows.iter()
.filter(|row| row.get("title").and_then(Value::as_str) == Some(title))
.max_by_key(|row| row.get("lastOutputAt").and_then(Value::as_i64).unwrap_or(0))
.and_then(|row| TabKeys::read(row).handle)
}
/// The error for a `terminal create` whose response named no handle, with
/// the tab it made rolled back first.
///
/// The tab exists by the time this runs, so it is the one way a failed spawn
/// can leak a resource: the handle is recovered from the coordinates the
/// response did carry and the tab is closed. A tab that cannot be located is
/// named by those coordinates in the error instead, so an operator can
/// finish the job, and no other tab is touched.
pub(super) fn roll_back_create(
&self,
keys: &TabKeys,
title: &str,
value: &Value,
) -> anyhow::Error {
let coordinates = format!(
"title {title:?}, tab_id {:?}, leaf_id {:?}, pane_key {:?}",
keys.tab_id, keys.leaf_id, keys.pane_key
);
match self.find_created(keys, title) {
Some(handle) => match self.close_handle(&handle) {
Ok(()) => anyhow::anyhow!(
"orca terminal create returned no handle; the tab it made ({coordinates}, \
handle {handle}) was closed: {value}"
),
Err(error) => anyhow::anyhow!(
"orca terminal create returned no handle and the tab it made ({coordinates}, \
handle {handle}) was not closed: {error}; close it by hand. create \
answered: {value}"
),
},
None => anyhow::anyhow!(
"orca terminal create returned no handle and the tab it made ({coordinates}) \
could not be located to close; close it by hand. create answered: {value}"
),
}
}
/// The freshest `terminal show` payload for a session, re-resolving the
/// stored handle once when Orca answers that it went stale.
///
/// A stale handle is expected after an Orca runtime restart: the pane keeps
/// its `pane_key` while the PTY incarnation mints a new `term_<uuid>`. The
/// returned [`SessionRef`] carries the handle the payload belongs to.
pub(super) fn current(&self, session: &SessionRef) -> Result<(SessionRef, Value)> {
if let Some(handle) = ref_str(session, "handle") {
match self.show(&handle) {
Ok(value) => return Ok((session.clone(), value)),
Err(error) if !is_stale(&error) => return Err(error),
Err(_) => {}
}
}
let refreshed = self.remint(session)?;
let handle = ref_str(&refreshed, "handle")
.ok_or_else(|| anyhow::anyhow!("orca remint produced no handle"))?;
let value = self.show(&handle)?;
self.note(&refreshed, "spawned");
Ok((refreshed, value))
}
}