mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
fix(core): route Codex reports past /new and codex -C, trust agent_settled only once seen
A session /new started goes to the Codex pane that had one; a report from a directory no pane runs Codex in is matched among every Codex pane instead of falling back to the app-server's. The Pi bridge no longer reads a returned function from on() as proof agent_settled will come, and the Stop hook re-reads the transcript only once it has grown.
This commit is contained in:
@@ -153,6 +153,9 @@ fn build_hook_body(agent: &str, event: &str, stdin_json: &str) -> String {
|
||||
("cwd", "cwd"),
|
||||
// Goose spells the working directory its own way.
|
||||
("cwd", "working_dir"),
|
||||
// What started a session (startup, resume, clear…): routes a Codex
|
||||
// report — see `daemon::pane::codex_report_target`.
|
||||
("source", "source"),
|
||||
] {
|
||||
if let Some(v) = payload
|
||||
.get(key)
|
||||
@@ -336,14 +339,20 @@ fn catalog_says_no_effort(catalogs: &[serde_json::Value], model: &str) -> bool {
|
||||
|
||||
/// The model that wrote `reply` to the prompt `prompt_id`, once the
|
||||
/// transcript has it as its latest words. Claude Code runs `Stop` before
|
||||
/// that write is always on disk, so this waits a little for it.
|
||||
/// that write is always on disk, so this waits a little for it — reading
|
||||
/// the tail again only once the transcript has grown.
|
||||
fn turn_reply_model(path: &Path, reply: &str, prompt_id: Option<&str>) -> Option<String> {
|
||||
let deadline = std::time::Instant::now() + TRANSCRIPT_CATCH_UP;
|
||||
let mut read = None;
|
||||
loop {
|
||||
if let Some(model) =
|
||||
transcript_tail(path).and_then(|t| reply_model_in(&t, reply, prompt_id))
|
||||
{
|
||||
return Some(model);
|
||||
let len = std::fs::metadata(path).ok().map(|m| m.len());
|
||||
if len != read {
|
||||
read = len;
|
||||
if let Some(model) =
|
||||
transcript_tail(path).and_then(|t| reply_model_in(&t, reply, prompt_id))
|
||||
{
|
||||
return Some(model);
|
||||
}
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return None;
|
||||
@@ -2217,9 +2226,10 @@ export default function (pi: ExtensionAPI) {{
|
||||
// Whether this build says when a run is over for good. agent_end also ends
|
||||
// each attempt an automatic retry, a compaction or a queued message picks
|
||||
// up again, and reading that as "done" calls the pane finished while it
|
||||
// still works. Pi's agent_settled comes once the run is really over; a Pi
|
||||
// whose on() hands back an unsubscribe is new enough to always send it,
|
||||
// and an older one that sends it shows so the first time it does.
|
||||
// still works. Pi's agent_settled comes once the run is really over, and a
|
||||
// build that sends it shows so the first time it does. Nothing earlier
|
||||
// tells: on() returns nothing in Pi or Oh My Pi, and a build that takes the
|
||||
// name without ever sending it must still end its turns on agent_end.
|
||||
let settles = false;
|
||||
pi.on("agent_start", (_event, ctx) => emit((turn = "prompt-submit"), ctx));
|
||||
pi.on("agent_end", (_event, ctx) => {{
|
||||
@@ -2250,11 +2260,10 @@ export default function (pi: ExtensionAPI) {{
|
||||
pi.on("ui_prompt_end", (_event, ctx) => emit(turn, ctx));
|
||||
}} catch {{}}
|
||||
try {{
|
||||
const off: unknown = pi.on("agent_settled" as any, (_event: unknown, ctx: SessionCtx) => {{
|
||||
pi.on("agent_settled" as any, (_event: unknown, ctx: SessionCtx) => {{
|
||||
settles = true;
|
||||
emit((turn = "stop"), ctx);
|
||||
}});
|
||||
if (typeof off === "function") settles = true;
|
||||
}} catch {{}}
|
||||
// Oh My Pi asks before running a tool through events of its own rather
|
||||
// than Pi's UI-prompt pair, and announces its retries — which follow an
|
||||
|
||||
@@ -3609,14 +3609,17 @@ pub struct CodexCandidate {
|
||||
/// as long as that session lasts, so neither says which pane the report is
|
||||
/// about. The report does: its session, then its directory. A pane that has
|
||||
/// already reported the session owns it; otherwise it is the one pane running
|
||||
/// Codex in that directory — the one yet to report a session, when there are
|
||||
/// several, and the named pane when that still leaves a tie. `None` is no
|
||||
/// pane, or no telling which.
|
||||
/// Codex in that directory, or among every Codex pane when none runs there
|
||||
/// (`codex -C` leaves the shell elsewhere). Of several, a session `/new`
|
||||
/// started (`cleared`) goes to one that had a session before and any other to
|
||||
/// one yet to report, and the named pane breaks a tie that still stands.
|
||||
/// `None` is no pane, or no telling which.
|
||||
pub fn codex_report_target(
|
||||
panes: &[CodexCandidate],
|
||||
named: u64,
|
||||
session: Option<&str>,
|
||||
cwd: Option<&Path>,
|
||||
cleared: bool,
|
||||
) -> Option<u64> {
|
||||
let codex = || panes.iter().filter(|p| p.runs_codex);
|
||||
if let Some(session) = session
|
||||
@@ -3624,16 +3627,23 @@ pub fn codex_report_target(
|
||||
{
|
||||
return Some(owner.pane);
|
||||
}
|
||||
let cwd = cwd?;
|
||||
let here: Vec<&CodexCandidate> = codex()
|
||||
.filter(|p| p.cwd.as_deref().is_some_and(|c| same_dir(c, cwd)))
|
||||
.filter(|p| {
|
||||
cwd.zip(p.cwd.as_deref())
|
||||
.is_some_and(|(cwd, c)| same_dir(c, cwd))
|
||||
})
|
||||
.collect();
|
||||
let fresh: Vec<&CodexCandidate> = here
|
||||
let pool = if here.is_empty() {
|
||||
codex().collect()
|
||||
} else {
|
||||
here
|
||||
};
|
||||
let likely: Vec<&CodexCandidate> = pool
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|p| p.session.is_none())
|
||||
.filter(|p| p.session.is_some() == cleared)
|
||||
.collect();
|
||||
let pool = if fresh.is_empty() { here } else { fresh };
|
||||
let pool = if likely.is_empty() { pool } else { likely };
|
||||
match pool.as_slice() {
|
||||
[only] => Some(only.pane),
|
||||
[] => None,
|
||||
@@ -4238,7 +4248,7 @@ mod tests {
|
||||
pane(5, true, "/work/c", None),
|
||||
];
|
||||
let target = |named: u64, session: Option<&str>, cwd: &str| {
|
||||
codex_report_target(&panes, named, session, Some(Path::new(cwd)))
|
||||
codex_report_target(&panes, named, session, Some(Path::new(cwd)), false)
|
||||
};
|
||||
|
||||
// The app-server was started in pane 1, so every hook names it.
|
||||
@@ -4263,14 +4273,39 @@ mod tests {
|
||||
Some(1),
|
||||
"a new session in a pane that had one before (/new) is still that pane's"
|
||||
);
|
||||
assert_eq!(target(1, Some("s-new"), "/work/none"), None);
|
||||
assert_eq!(
|
||||
codex_report_target(&panes, 1, Some("s-c2"), Some(Path::new("/work/c")), true),
|
||||
Some(4),
|
||||
"but a session /new started goes to the one that had a session"
|
||||
);
|
||||
assert_eq!(
|
||||
target(1, Some("s-new"), "/work/none"),
|
||||
None,
|
||||
"a directory no pane runs Codex in leaves every fresh pane in the running"
|
||||
);
|
||||
let elsewhere = [
|
||||
pane(1, true, "/work/a", Some("s-a")),
|
||||
pane(2, true, "/home", None),
|
||||
];
|
||||
assert_eq!(
|
||||
codex_report_target(
|
||||
&elsewhere,
|
||||
1,
|
||||
Some("s-b"),
|
||||
Some(Path::new("/work/b")),
|
||||
false
|
||||
),
|
||||
Some(2),
|
||||
"codex -C: the one Codex pane yet to report, not the app-server's"
|
||||
);
|
||||
|
||||
let twins = [
|
||||
pane(6, true, "/work/d", None),
|
||||
pane(7, true, "/work/d", None),
|
||||
];
|
||||
let twin =
|
||||
|named| codex_report_target(&twins, named, Some("s"), Some(Path::new("/work/d")));
|
||||
let twin = |named| {
|
||||
codex_report_target(&twins, named, Some("s"), Some(Path::new("/work/d")), false)
|
||||
};
|
||||
assert_eq!(twin(7), Some(7), "a tie goes to the pane the hook named");
|
||||
assert_eq!(twin(1), None, "and is no one's otherwise");
|
||||
}
|
||||
|
||||
@@ -146,6 +146,10 @@ fn codex_report_pane(
|
||||
named,
|
||||
event.session_id.as_deref(),
|
||||
event.cwd.as_deref(),
|
||||
// Codex's SessionStart says what started the session; `clear` is /new.
|
||||
serde_json::from_str::<serde_json::Value>(body)
|
||||
.ok()
|
||||
.is_some_and(|body| body["source"] == "clear"),
|
||||
)?;
|
||||
let pane = panes.into_iter().find(|p| p.id == target)?;
|
||||
Some((pane, event))
|
||||
|
||||
Reference in New Issue
Block a user