Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/stella-observatory/src/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,13 @@ <h2>${fmtInt(runs)} undated ${runs === 1 ? "run" : "runs"}
if (e.type === "stage") return `<div class="jrnl-stage">stage · ${esc(e.label ?? "")}</div>`;
if (e.type === "speculation_discarded")
return `<div class="jrnl"><div class="kick">◌ speculative ${esc(e.name ?? "")} discarded · ${esc(e.reason ?? "")}</div></div>`;
/* A parked span explains a wall-clock gap that contains no other events
(#1857) — without its own arm it would fall through to "answer" and draw
a blank row exactly where the transcript most needs to say something. */
if (e.type === "turn_parked")
return `<div class="jrnl"><div class="kick">⏳ parked · ${esc(e.description ?? "")} · every ${esc(String(e.poll_interval_secs ?? "?"))}s, up to ${esc(String(e.deadline_secs ?? "?"))}s</div></div>`;
if (e.type === "turn_woken")
return `<div class="jrnl"><div class="kick">▶ woke · ${e.reason === "changed" ? "the watched state changed" : e.reason === "deadline_expired" ? "the deadline expired with no change" : esc(e.reason ?? "")} · ${esc(String(e.polls_used ?? 0))} probe(s)</div></div>`;
const head =
e.type === "tool_start" ? `▸ ${esc(e.name ?? "")}` :
e.type === "tool_result" ? `${e.ok ? "✓" : "✕"} result${e.duration_ms != null ? ` · ${fmtMs(e.duration_ms)}` : ""}${e.speculated ? " · speculated" : ""}` :
Expand Down
16 changes: 15 additions & 1 deletion crates/stella-observatory/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ impl Observatory {
WHERE execution_id = ?1
AND seq > ?2
AND event_type IN ('stage', 'text', 'reasoning', 'tool_start',
'tool_result', 'speculation_discarded')
'tool_result', 'speculation_discarded',
'turn_parked', 'turn_woken')
ORDER BY seq ASC";
let mut stmt = match conn.prepare(sql) {
Ok(stmt) => stmt,
Expand Down Expand Up @@ -1228,6 +1229,19 @@ fn journal_entry(row: Value, full: bool) -> Value {
out["name"] = payload["name"].clone();
out["reason"] = payload["reason"].clone();
}
// A parked span is the one thing that explains a wall-clock gap with
// no events in it (#1857). Without its payload the row would say a
// park happened but not what was waited on or for how long — which
// is the entire question an operator opens this transcript to ask.
"turn_parked" => {
out["description"] = payload["description"].clone();
out["poll_interval_secs"] = payload["poll_interval_secs"].clone();
out["deadline_secs"] = payload["deadline_secs"].clone();
}
"turn_woken" => {
out["reason"] = payload["reason"].clone();
out["polls_used"] = payload["polls_used"].clone();
}
_ => {}
}
out
Expand Down
2 changes: 1 addition & 1 deletion crates/stella-protocol/src/event/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1486,4 +1486,4 @@ fn a_known_event_wire_format_is_unchanged_by_the_fallback() {
assert!(matches!(back, AgentEvent::Text { text } if text == "hello"));
}

mod tag_table;
mod tag_table;
21 changes: 21 additions & 0 deletions crates/stella-tui/src/fleet_dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ enum LastAction {
Message(String),
/// Waiting on a human — approval, a secret, a decision.
Blocked(String),
/// Parked on an engine-side wait (#1857): the worker is probing external
/// state on its own clock and will resume itself.
///
/// Deliberately not [`LastAction::Blocked`], which means a human must act.
/// A park needs nobody — reading it as blocked would send an operator
/// looking for an approval prompt that does not exist.
Parked(String),
/// A non-retryable error headline.
Error(String),
}
Expand Down Expand Up @@ -295,6 +302,7 @@ impl TaskRow {
LastAction::Thinking => "thinking…".to_string(),
LastAction::Message(s) => s.clone(),
LastAction::Blocked(reason) => format!("waiting: {reason}"),
LastAction::Parked(desc) => format!("parked: {desc}"),
LastAction::Error(msg) => msg.clone(),
}
}
Expand Down Expand Up @@ -457,6 +465,19 @@ impl FleetBoard {
row.action = LastAction::Message(line);
}
}
// A park with no arm here would freeze the row on whatever tool
// ran last, so an operator watching a fleet would read a worker
// that is deliberately waiting as one that is stuck mid-tool.
// The status stays whatever it was — a park is the worker running,
// not blocked (nobody has to act) and not terminal.
AgentEvent::TurnParked { description, .. } => {
row.action = LastAction::Parked(first_line(description));
}
// The wake carries no subject of its own; the next tool or message
// repaints the row a beat later, so clearing to `Idle` here would
// only flicker. Holding the park until then reads as "it woke and
// is picking back up", which is what happened.
AgentEvent::TurnWoken { .. } => {}
AgentEvent::AskUser { question, .. } => {
row.action = LastAction::Blocked(first_line(question));
if !row.status.is_terminal() {
Expand Down
Loading