Reconcile Claude background task state
This commit is contained in:
1 parent
f0661919bb
commit
9fd21af4e8
6 files changed
+310
-24
No files matched your search
@@ -261,13 +261,15 @@ written, and the fold uses that same predicate to decide a reply is settled.
|
||||
mid-sentence. `./ui-sandbox.sh` plus `/subagent 3` or `/background 5` in an
|
||||
echo session is the whole rig; the helpers stagger a second apart so each
|
||||
reply is its own.
|
||||
- **Whether work is outstanding has two sources and needs both.** The
|
||||
translator's `open_tasks` is what it watched start — the only thing that
|
||||
knows about a backgrounded command — and `Subagents::any_open` reads the
|
||||
directory, which is the only thing that knows about a subagent started
|
||||
before this translator existed. That second one is every subagent a session
|
||||
has when the backend is updated under it: adoption reads stdout from a
|
||||
recorded offset, so those `task_started` lines are already behind it.
|
||||
- **Claude's background-task level is authority; two edge sources are the
|
||||
fallback.** Since Claude Code 2.1.261,
|
||||
`background_tasks_changed { tasks: [...] }` replaces the live set and repairs
|
||||
a missed ending edge. An adopted CLI is sent a repeated `initialize` to ask
|
||||
for the current set. Reconcile only between turns or at a result boundary:
|
||||
a foreground agent is legitimately absent from a background-only snapshot.
|
||||
Older CLIs still need both edge sources: `open_tasks` knows about a
|
||||
backgrounded command, while `Subagents::any_open` finds a subagent whose
|
||||
`task_started` is behind an adopted stdout offset.
|
||||
- **A usage limit a subagent hits reaches the session**, not just the
|
||||
subagent's own transcript; auto-resume can only schedule against a session.
|
||||
That is the case where the main agent is idle and a background Task is
|
||||
|
||||
@@ -870,6 +870,19 @@ rather than left making a claim nothing will ever correct — including for the
|
||||
endings that carry no summary, which are exactly the ones that went wrong and
|
||||
the ones a stale "running in background" reads worst on.
|
||||
|
||||
**The edge stream is detail; Claude's background-task level is authority**
|
||||
(2026-09-15). Claude Code 2.1.261 added
|
||||
`background_tasks_changed { tasks: [...] }` with replace semantics expressly so
|
||||
a missed bookend cannot wedge a running indicator. Its ids are not correlated
|
||||
with the edge stream; this side uses the authoritative empty/nonempty level.
|
||||
The driver sends a repeated `initialize` when it adopts a CLI, which prompts a
|
||||
full snapshot without restarting the conversation. A parent already recorded as idle or waiting can
|
||||
apply it immediately; one adopted mid-turn waits for the result boundary,
|
||||
because a foreground agent is correctly absent from a background-only set.
|
||||
The ordinary notifications still supply outcomes and summaries, including when
|
||||
one is ordered after the level already corrected the status. Older CLIs send no
|
||||
level and retain the edge fallback below.
|
||||
|
||||
What the notification is still used for is the status: it is what closes a task
|
||||
in `Status::Waiting`'s bookkeeping. Handled once, however many of the two
|
||||
lifecycle shapes (`task_notification`, `task_updated`) arrive — whichever gets
|
||||
|
||||
+20
-6
@@ -86,6 +86,20 @@ transcript is still being written to and its process is the session's to stop.
|
||||
status ends it from the update, since the failure to avoid is a subagent
|
||||
nothing ever finishes.
|
||||
|
||||
Since Claude Code 2.1.261, `background_tasks_changed { tasks: [...] }` is
|
||||
the authoritative level beside those edges: its set replaces the previous
|
||||
set, so a missed terminal edge cannot leave a subagent running forever. Its
|
||||
ids are deliberately not correlated with the edge stream; the useful claim
|
||||
here is whether the set is empty. The edges still carry mapping, outcome
|
||||
and closing summary. On adoption the driver sends a repeated `initialize`,
|
||||
which makes a current CLI send the full set; an older CLI accepts it and sends no level,
|
||||
leaving the edge-based path unchanged. A snapshot is reconciled immediately
|
||||
when the persisted parent status proves it is between turns, and otherwise
|
||||
at the next `result` boundary -- while a turn is open, a foreground agent is
|
||||
legitimately absent from the background set. Reconciliation writes
|
||||
`Status Exited`, which is also what makes a formerly stale row deletable;
|
||||
a task notification ordered after the level can still add its summary.
|
||||
|
||||
The two rules this replaces were both wrong, in opposite directions. The
|
||||
parent's `tool_result` is not it: a backgrounded Task's arrives at launch
|
||||
("Async agent launched..."), so ending there truncated a running agent's
|
||||
@@ -117,12 +131,12 @@ transcript is still being written to and its process is the session's to stop.
|
||||
|
||||
**While any task is outstanding the session's turn ends in
|
||||
`Status Waiting` rather than `Idle`.** `Idle` means "waiting for a person",
|
||||
and a session with a backgrounded subagent is not doing that. Two sources:
|
||||
the translator's `open_tasks`, and `Subagents::any_open` -- which is what
|
||||
covers a subagent launched before a backend restart adopted the session,
|
||||
whose `task_started` is behind the offset its stdout is read from. That
|
||||
second lookup is also what lets such a subagent's ending be recognised at
|
||||
all.
|
||||
and a session with a backgrounded subagent is not doing that. The edge
|
||||
fallback has two sources: the translator's `open_tasks`, and
|
||||
`Subagents::any_open` -- which covers a subagent launched before a backend
|
||||
restart adopted the session, whose `task_started` is behind the durable
|
||||
stdout offset. On current Claude versions the replace-semantics level above
|
||||
reconciles both at a safe turn boundary.
|
||||
|
||||
**A limit the account hits inside a subagent is hoisted to the session**
|
||||
as well as recorded here, because `resume.rs` can only schedule against a
|
||||
|
||||
@@ -214,6 +214,7 @@ impl ClaudeDriver {
|
||||
session_dir: &Path,
|
||||
sink: EventSink,
|
||||
subagents: Arc<Subagents>,
|
||||
initial_status: SessionStatus,
|
||||
) -> Result<Self> {
|
||||
let state = Arc::new(Mutex::new(Translator::new(
|
||||
session_dir.to_path_buf(),
|
||||
@@ -261,6 +262,9 @@ impl ClaudeDriver {
|
||||
Self::start(meta, provider, transport, session_dir)?
|
||||
}
|
||||
};
|
||||
if !started_here && matches!(initial_status, SessionStatus::Idle | SessionStatus::Waiting) {
|
||||
state.lock().unwrap().mark_settled();
|
||||
}
|
||||
|
||||
// A process this driver has just started has been asked for nothing,
|
||||
// which is what idle means. Said here because nothing else will: the
|
||||
@@ -320,14 +324,22 @@ impl ClaudeDriver {
|
||||
format!("{} {}", provider.name, transport.describe()),
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
let driver = Self {
|
||||
sink,
|
||||
queue,
|
||||
to_child,
|
||||
state,
|
||||
session_dir: session_dir.to_path_buf(),
|
||||
reading,
|
||||
})
|
||||
};
|
||||
if !started_here {
|
||||
// Claude 2.1.261 sends a full background-task snapshot after a
|
||||
// repeated initialize. That repairs a completion edge missed by a
|
||||
// backend that was down while the CLI kept running; older CLIs
|
||||
// accept the request and simply send no snapshot.
|
||||
driver.send_control(json!({"subtype": "initialize"}), None);
|
||||
}
|
||||
Ok(driver)
|
||||
}
|
||||
|
||||
/// Starts a new CLI for this session, with its streams in the session
|
||||
@@ -866,8 +878,12 @@ fn translate_line(
|
||||
return true;
|
||||
};
|
||||
let opens_a_model_call = starts_a_model_call(&message);
|
||||
let parent_running = queue.lock().unwrap().running;
|
||||
let (events, new_session_id, before) = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if parent_running {
|
||||
state.mark_running();
|
||||
}
|
||||
let before = state.session_id.clone();
|
||||
let events = state.translate(&message);
|
||||
let after = state.session_id.clone();
|
||||
|
||||
@@ -63,6 +63,14 @@ struct PendingRequest {
|
||||
answers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Where a terminal notification's detail still belongs after the level
|
||||
/// signal has already closed the task.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum TaskReport {
|
||||
Subagent,
|
||||
Command,
|
||||
}
|
||||
|
||||
/// Translation state: stream-json lines in, common events out.
|
||||
pub(super) struct Translator {
|
||||
pub(super) session_id: Option<String>,
|
||||
@@ -125,6 +133,16 @@ pub(super) struct Translator {
|
||||
/// translator existed, which is every one of them after a backend
|
||||
/// restart adopts a running session.
|
||||
open_tasks: HashSet<String>,
|
||||
/// Claude's level signal for background work, when this CLI is new enough
|
||||
/// to send one. Unlike `open_tasks`, this is a snapshot: each new value
|
||||
/// replaces the old one, so a missed ending edge cannot leave work open
|
||||
/// forever. See [`Translator::translate_background_tasks`].
|
||||
background_tasks: Option<bool>,
|
||||
/// Tasks the level signal closed before their ordinary notification
|
||||
/// arrived. That notification still owns the useful summary, so it gets
|
||||
/// one chance to update the transcript or tool card after the status was
|
||||
/// already corrected.
|
||||
awaiting_task_summaries: HashMap<String, TaskReport>,
|
||||
/// File-edit calls whose successful boilerplate result should not be drawn below their diff.
|
||||
/// Each leaves here with its `tool_result`; a failure keeps its text because that is the part a
|
||||
/// reader needs to act on.
|
||||
@@ -139,6 +157,10 @@ pub(super) struct Translator {
|
||||
/// narrow: whether a task reporting back means the session has gone idle,
|
||||
/// or is merely one of several things happening inside a turn.
|
||||
in_turn: bool,
|
||||
/// Whether this translator has observed that the parent is between turns.
|
||||
/// False on construction because an adopted process may be mid-turn; the
|
||||
/// driver sets it only when the persisted session status proves otherwise.
|
||||
settled: bool,
|
||||
}
|
||||
|
||||
impl Translator {
|
||||
@@ -155,11 +177,28 @@ impl Translator {
|
||||
rate_limited: false,
|
||||
tasks: HashMap::new(),
|
||||
open_tasks: HashSet::new(),
|
||||
background_tasks: None,
|
||||
awaiting_task_summaries: HashMap::new(),
|
||||
patches: HashSet::new(),
|
||||
in_turn: false,
|
||||
settled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The persisted parent status says an adopted process is between turns,
|
||||
/// so an initial background-task snapshot is safe to apply immediately.
|
||||
pub(super) fn mark_settled(&mut self) {
|
||||
self.settled = true;
|
||||
}
|
||||
|
||||
/// The driver has a parent turn in flight. This covers the interval before
|
||||
/// its first output proves the same thing, when a background-task update
|
||||
/// from an older turn must not briefly return the session to idle.
|
||||
pub(super) fn mark_running(&mut self) {
|
||||
self.in_turn = true;
|
||||
self.settled = false;
|
||||
}
|
||||
|
||||
/// Remembers what a control request was for, so its answer can say so.
|
||||
/// Called before the request goes out: the reader thread is already running
|
||||
/// and a fast CLI can answer before this side gets back to it.
|
||||
@@ -263,8 +302,10 @@ impl Translator {
|
||||
let events = self.translate_line(message);
|
||||
if events.iter().any(closes_a_turn) {
|
||||
self.in_turn = false;
|
||||
self.settled = true;
|
||||
} else if events.iter().any(super::proves_a_turn) {
|
||||
self.in_turn = true;
|
||||
self.settled = false;
|
||||
}
|
||||
events
|
||||
}
|
||||
@@ -388,6 +429,10 @@ impl Translator {
|
||||
if tokens > 0 {
|
||||
events.push(Event::UsageDelta { tokens, context });
|
||||
}
|
||||
// A level snapshot is authoritative at a turn boundary. In
|
||||
// particular, it repairs a task whose terminal edge was
|
||||
// missed before this backend adopted the still-running CLI.
|
||||
events.extend(self.reconcile_background_tasks());
|
||||
// Idle means "waiting for a person", and a session with a
|
||||
// backgrounded subagent or command still running is not doing
|
||||
// that -- it is waiting for itself, and will speak again with
|
||||
@@ -450,6 +495,7 @@ impl Translator {
|
||||
Some("task_started" | "task_progress" | "task_updated" | "task_notification") => {
|
||||
self.translate_task(message)
|
||||
}
|
||||
Some("background_tasks_changed") => self.translate_background_tasks(message),
|
||||
Some("compact_boundary") => {
|
||||
let meta = &message["compact_metadata"];
|
||||
vec![Event::Compacted {
|
||||
@@ -582,6 +628,7 @@ impl Translator {
|
||||
// parent's own message, which arrives first and carries the
|
||||
// title this side shows.
|
||||
if let Some(about) = about {
|
||||
self.awaiting_task_summaries.remove(&about);
|
||||
self.open_tasks.insert(about);
|
||||
}
|
||||
Vec::new()
|
||||
@@ -592,19 +639,87 @@ impl Translator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 2.1.261's authoritative account of whether background work is
|
||||
/// alive. The `tasks` array has replace semantics, but its ids are not
|
||||
/// promised to correlate with task edges, so only its emptiness is used.
|
||||
fn translate_background_tasks(&mut self, message: &Value) -> Vec<Event> {
|
||||
let Some(tasks) = message.get("tasks").and_then(Value::as_array) else {
|
||||
tracing::warn!("background_tasks_changed without a tasks array");
|
||||
return Vec::new();
|
||||
};
|
||||
let was_outstanding = self.work_outstanding();
|
||||
// The CLI explicitly says not to correlate these ids with its edge
|
||||
// stream. Their useful claim here is the level: empty or not.
|
||||
self.background_tasks = Some(!tasks.is_empty());
|
||||
|
||||
// During a turn the snapshot omits a foreground task, so wait for the
|
||||
// result boundary before using it to close anything. Between turns,
|
||||
// every task that can still be alive is background work and the level
|
||||
// can repair a missed notification immediately.
|
||||
if !self.settled || self.in_turn {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut events = self.reconcile_background_tasks();
|
||||
let is_outstanding = self.work_outstanding();
|
||||
if was_outstanding != is_outstanding {
|
||||
events.push(Event::Status {
|
||||
state: if is_outstanding {
|
||||
SessionStatus::Waiting
|
||||
} else {
|
||||
SessionStatus::Idle
|
||||
},
|
||||
});
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
/// Applies the latest background-task snapshot at a point where no
|
||||
/// foreground task can remain. Returns updates for background commands;
|
||||
/// subagents carry the same correction in their own status transcript.
|
||||
fn reconcile_background_tasks(&mut self) -> Vec<Event> {
|
||||
let Some(false) = self.background_tasks else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut events = Vec::new();
|
||||
for info in self.subagents.list(true) {
|
||||
if info.status == SessionStatus::Running {
|
||||
self.awaiting_task_summaries
|
||||
.insert(info.id.clone(), TaskReport::Subagent);
|
||||
self.subagents.finish(&info.id);
|
||||
}
|
||||
}
|
||||
for id in self.open_tasks.drain() {
|
||||
let report = if self.subagents.get(&id).is_some() {
|
||||
TaskReport::Subagent
|
||||
} else {
|
||||
TaskReport::Command
|
||||
};
|
||||
self.awaiting_task_summaries.insert(id.clone(), report);
|
||||
if report == TaskReport::Command {
|
||||
events.push(Event::ToolUpdate {
|
||||
id,
|
||||
output: "this background command finished".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
/// Whether the session has work of its own still running: the difference
|
||||
/// between `Idle` and [`SessionStatus::Waiting`].
|
||||
///
|
||||
/// Two sources because neither covers the other. `open_tasks` holds what
|
||||
/// this translator watched start, which is the only thing that knows
|
||||
/// about a backgrounded *command* -- it has no subagent. The registry
|
||||
/// holds what is on disk, which is the only thing that knows about a
|
||||
/// subagent that started before this translator did.
|
||||
/// The level is authoritative when a current CLI has supplied it. The
|
||||
/// edge fallback has two sources because neither covers the other:
|
||||
/// `open_tasks` knows about a backgrounded command, which has no
|
||||
/// subagent, while the registry knows about a subagent started before
|
||||
/// this translator existed.
|
||||
///
|
||||
/// `session_running` is true by construction: this is only ever asked
|
||||
/// while translating a line the session's process just wrote.
|
||||
fn work_outstanding(&self) -> bool {
|
||||
!self.open_tasks.is_empty() || self.subagents.any_open(true)
|
||||
self.background_tasks == Some(true)
|
||||
|| !self.open_tasks.is_empty()
|
||||
|| self.subagents.any_open(true)
|
||||
}
|
||||
|
||||
/// A task reporting back, from whichever of the two lines got here first.
|
||||
@@ -643,7 +758,9 @@ impl Translator {
|
||||
// `task_started` is behind the offset its session's stdout is read
|
||||
// from. `finish` below closes it either way, so a second line for the
|
||||
// same task still finds nothing.
|
||||
if !self.open_tasks.remove(&about) && !self.subagents.is_open(&about) {
|
||||
let reconciled = self.awaiting_task_summaries.remove(&about);
|
||||
let was_open = self.open_tasks.remove(&about) || self.subagents.is_open(&about);
|
||||
if !was_open && reconciled.is_none() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
@@ -661,7 +778,7 @@ impl Translator {
|
||||
self.subagents
|
||||
.record(&about, Event::AssistantText { delta: summary });
|
||||
}
|
||||
} else {
|
||||
} else if reconciled != Some(TaskReport::Subagent) {
|
||||
events.push(Event::ToolUpdate {
|
||||
id: about.clone(),
|
||||
// A task that stopped without a word still has to say so: the
|
||||
@@ -677,7 +794,7 @@ impl Translator {
|
||||
// over: it has stopped being `Waiting` and nothing else will say so.
|
||||
// Inside a turn there is nothing to announce -- the turn's own
|
||||
// `result` will decide between the two statuses when it lands.
|
||||
if !self.work_outstanding() && !self.in_turn {
|
||||
if was_open && !self.work_outstanding() && !self.in_turn {
|
||||
events.push(Event::Status {
|
||||
state: SessionStatus::Idle,
|
||||
});
|
||||
@@ -1724,6 +1841,126 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Claude's task edges are useful detail but not durable state. A
|
||||
/// repeated initialize after adoption sends this level snapshot, whose
|
||||
/// empty set is the authoritative answer even when the old registry says
|
||||
/// a subagent is still running.
|
||||
#[test]
|
||||
fn a_background_snapshot_repairs_an_adopted_waiting_session() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let subagents = test_subagents(&dir);
|
||||
subagents.start("toolu_stale", "an old helper", None);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translator.mark_settled();
|
||||
|
||||
assert!(
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"background_tasks_changed","tasks":["toolu_stale"]}"#],
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(subagents.list(true)[0].status, SessionStatus::Running);
|
||||
assert_eq!(
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"background_tasks_changed","tasks":[]}"#],
|
||||
),
|
||||
vec![Event::Status {
|
||||
state: SessionStatus::Idle
|
||||
}]
|
||||
);
|
||||
assert_eq!(subagents.list(true)[0].status, SessionStatus::Exited);
|
||||
}
|
||||
|
||||
/// An adopted process may be in the middle of a foreground agent when its
|
||||
/// initialize snapshot arrives. Foreground work is absent from that
|
||||
/// snapshot, so it is only safe to reconcile at the result boundary.
|
||||
#[test]
|
||||
fn a_background_snapshot_does_not_close_foreground_work_mid_turn() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let subagents = test_subagents(&dir);
|
||||
subagents.start("toolu_foreground", "foreground helper", None);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translator.mark_running();
|
||||
|
||||
assert!(
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"background_tasks_changed","tasks":[]}"#],
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(subagents.list(true)[0].status, SessionStatus::Running);
|
||||
|
||||
let result = r#"{"type":"result","subtype":"success","is_error":false,"usage":{}}"#;
|
||||
assert_eq!(
|
||||
translate_lines(&mut translator, &[result]).last(),
|
||||
Some(&Event::Status {
|
||||
state: SessionStatus::Idle
|
||||
})
|
||||
);
|
||||
assert_eq!(subagents.list(true)[0].status, SessionStatus::Exited);
|
||||
}
|
||||
|
||||
/// The level edge is deliberately allowed to arrive before the detailed
|
||||
/// notification. Correcting the status must not discard the useful report
|
||||
/// that follows it or emit a second idle transition.
|
||||
#[test]
|
||||
fn a_notification_after_the_level_correction_keeps_its_summary() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let subagents = test_subagents(&dir);
|
||||
subagents.start("toolu_level", "level helper", None);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translator.mark_settled();
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"background_tasks_changed","tasks":[]}"#],
|
||||
);
|
||||
|
||||
assert!(
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"task_notification","tool_use_id":"toolu_level","status":"completed","summary":"the useful report"}"#],
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
let transcript_path = subagents
|
||||
.get("toolu_level")
|
||||
.expect("subagent")
|
||||
.transcript_path();
|
||||
assert!(
|
||||
std::fs::read_to_string(transcript_path)
|
||||
.expect("read transcript")
|
||||
.contains("the useful report")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_deleted_reconciled_subagent_is_not_mistaken_for_a_command() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let subagents = test_subagents(&dir);
|
||||
subagents.start("toolu_deleted", "deleted helper", None);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translator.mark_settled();
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"background_tasks_changed","tasks":[]}"#],
|
||||
);
|
||||
subagents
|
||||
.delete(&["toolu_deleted".to_string()], true)
|
||||
.expect("delete corrected subagent");
|
||||
|
||||
assert!(
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[r#"{"type":"system","subtype":"task_notification","tool_use_id":"toolu_deleted","status":"completed","summary":"late"}"#],
|
||||
)
|
||||
.is_empty(),
|
||||
"a missing subagent is not a background command"
|
||||
);
|
||||
}
|
||||
|
||||
/// A task ending *inside* a turn says nothing about the session's status:
|
||||
/// the turn is still running, and its own `result` decides. Without the
|
||||
/// `in_turn` guard this reported the session idle in the middle of one,
|
||||
|
||||
@@ -1908,6 +1908,7 @@ impl SessionManager {
|
||||
session.transcript_path(),
|
||||
&session.sink,
|
||||
session.subagents(),
|
||||
status,
|
||||
)?);
|
||||
}
|
||||
// Nothing is live for this one -- a session whose launch failed
|
||||
@@ -2463,6 +2464,7 @@ fn launch(
|
||||
&transcript_path,
|
||||
&sink,
|
||||
&subagents,
|
||||
status,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
@@ -2514,6 +2516,7 @@ fn make_driver(
|
||||
transcript_path: &Path,
|
||||
sink: &EventSink,
|
||||
subagents: &Arc<Subagents>,
|
||||
initial_status: SessionStatus,
|
||||
) -> Result<Arc<dyn Driver>> {
|
||||
Ok(match provider.kind {
|
||||
DriverKind::Echo => Arc::new(EchoDriver::new(
|
||||
@@ -2541,6 +2544,7 @@ fn make_driver(
|
||||
dir,
|
||||
sink.clone(),
|
||||
Arc::clone(subagents),
|
||||
initial_status,
|
||||
)?),
|
||||
DriverKind::CodexCli => Arc::new(CodexDriver::launch(
|
||||
meta,
|
||||
|
||||
Reference in new issue
Block a user