From 6278379f37af24dc5087f72c649b2f9b27b8b00f Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 10 Jul 2026 09:33:58 -0400 Subject: [PATCH 1/2] fix(launch): stop appending reply-handoff footer to CLI launch prompts The hardcoded 'When done, send your result back to @' suffix contradicts dispatcher-authored prompts that specify a different reply target ('report to @X') or none at all ('no report needed'). Real incident: a spawned agent received both 'do not report back to me' in the prompt body and 'send your result back to @' in the suffix, and had to arbitrate on its own. The prompt author owns reply semantics. CLI launches now pass append_reply_handoff=false; the suffix logic is extracted into reply_handoff_suffix() with unit coverage. Resume/fork and relay paths keep their existing behavior. Co-Authored-By: Claude Fable 5 Session: 1836a587-1856-492d-bb20-ed6d5717f7ee Producer: ai-session Tool: codex --- src/commands/launch.rs | 5 ++++- src/launcher.rs | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/commands/launch.rs b/src/commands/launch.rs index 8773ebb3..b7834eeb 100644 --- a/src/commands/launch.rs +++ b/src/commands/launch.rs @@ -198,7 +198,10 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result { name: None, // --name is caller identity, not instance name skip_validation: false, terminal, - append_reply_handoff: true, + // CLI launches never append the reply footer: the dispatcher's + // prompt owns the reply semantics (a hardcoded footer contradicts + // prompts that say "report to X" or "no report needed"). + append_reply_handoff: false, }, )?; diff --git a/src/launcher.rs b/src/launcher.rs index 45a94b6d..2077f88d 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1737,15 +1737,10 @@ pub fn launch(db: &HcomDb, mut params: LaunchParams) -> Result { }); // Inject --hcom-prompt into tool args (translated per-tool). - // When a real hcom participant launched us, append a reply instruction so - // the spawned agent knows to send its result back. + // When a real hcom participant launched us, optionally append a reply + // instruction so the spawned agent knows to send its result back. if let Some(ref prompt) = params.initial_prompt { - let reply_suffix = - if params.append_reply_handoff && launcher_name != "api" && launcher_name != "user" { - format!("\n\nWhen done, send your result back to @{launcher_name} via hcom.") - } else { - String::new() - }; + let reply_suffix = reply_handoff_suffix(params.append_reply_handoff, &launcher_name); let full_prompt = format!("{prompt}{reply_suffix}"); append_initial_prompt_args(&normalized, &mut params.args, full_prompt)?; } @@ -2361,6 +2356,17 @@ fn cleanup_instance(db: &HcomDb, name: &str, process_id: &str) { db.delete_process_binding(process_id).ok(); } +/// Reply-handoff suffix appended after the initial prompt. The prompt is +/// authored by the dispatcher; any reply instruction written there must stay +/// authoritative, so callers opt in explicitly instead of always appending. +fn reply_handoff_suffix(append: bool, launcher_name: &str) -> String { + if append && launcher_name != "api" && launcher_name != "user" { + format!("\n\nWhen done, send your result back to @{launcher_name} via hcom.") + } else { + String::new() + } +} + #[cfg(test)] mod tests { use super::*; @@ -3399,4 +3405,18 @@ mod tests { let unix = sidecar_ambient_env(&env, strip.iter().copied(), false); assert!(!unix.contains_key("NO_COLOR") && unix.contains_key("no_color")); // Unix exact-case preserved } + + #[test] + fn test_reply_handoff_suffix_opt_in_only() { + // Opted out: prompt must stay exactly as authored, whoever launched us. + assert_eq!(reply_handoff_suffix(false, "huro"), ""); + // Opted in from a real participant: suffix names the launcher. + assert_eq!( + reply_handoff_suffix(true, "huro"), + "\n\nWhen done, send your result back to @huro via hcom." + ); + // Never appended for non-participant launchers, even when opted in. + assert_eq!(reply_handoff_suffix(true, "api"), ""); + assert_eq!(reply_handoff_suffix(true, "user"), ""); + } } From cf18385bd9a5bd2d81055abec2397ffe6b07a42e Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 10 Jul 2026 11:01:16 -0400 Subject: [PATCH 2/2] fix(resume): stop appending reply-handoff footer on resume/fork paths The hardcoded 'When done, send your result back to @' footer was still appended on plain resume (hcom r) and claude fork (hcom f); only the codex tracked-fork case was exempt (its identity-reset prompt used the initial_prompt slot). This contradicts dispatcher-authored prompts that specify a different reply target ('report to @X') or none at all ('no report needed'), the same conflict fixed for CLI launches in 449d33a. Fork sessions don't even receive the reception-discipline primer, so there is no downstream rule to override a stray footer. The prompt author owns reply semantics: build_resume_prompts now returns append_reply_handoff=false for every tool and path (resume, fork, adoption), matching the launch path. Adoption is not special-cased: a bare adoption with no -p has no prompt to append to anyway, and an adoption with -p lets that prompt own the semantics. Adds a regression test asserting no footer for resume, claude/codex fork, and adoption. Co-Authored-By: Claude Fable 5 Session: 0fe69845-16cf-409b-a9c7-02fa6bf0023e Producer: ai-session Tool: cc --- src/commands/resume.rs | 64 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/commands/resume.rs b/src/commands/resume.rs index ad2e3e31..9840c72c 100644 --- a/src/commands/resume.rs +++ b/src/commands/resume.rs @@ -553,9 +553,8 @@ fn prepare_resume_plan_from_source( name: launch_name, skip_validation: false, terminal: launch_flags.terminal.clone(), - // Codex tracked-instance fork uses initial_prompt for an identity - // reset; don't dilute it with a reply-handoff suffix. Adoption-fork - // has no identity-reset prompt, so normal handoff rules apply. + // Always false on resume/fork: the prompt author owns reply + // semantics (see build_resume_prompts). Matches the CLI launch path. append_reply_handoff, }, last_event_id, @@ -846,9 +845,15 @@ fn build_resume_prompts(input: ResumePromptInput<'_>) -> (Option, Option (None, _) => None, }; - // Codex tracked-instance fork uses initial_prompt for an identity reset; - // don't dilute it with a reply-handoff suffix. Adoption-fork has no reset. - let append_reply_handoff = !(fork && tool == "codex" && !is_adoption); + // Resume/fork never append the reply-handoff footer, for any tool. The + // prompt author owns reply semantics: the custom -p prompt (or the codex + // identity-reset prompt) is authoritative, and a hardcoded "send your + // result back to @" footer contradicts prompts that say + // "report to X" or "no report needed". This matches the CLI launch path, + // which also opts out. Fork sessions don't even receive the reception- + // discipline primer, so there is no downstream rule to override a stray + // footer — all the more reason not to inject one. + let append_reply_handoff = false; (system_prompt, initial_prompt, append_reply_handoff) } @@ -2310,6 +2315,53 @@ mod tests { assert_eq!(args, s(&["resume", "sess-456"])); } + // Reply-handoff footer must never be appended on resume/fork, for any tool + // or path. The prompt author owns reply semantics; a hardcoded + // "send your result back to @" footer contradicts prompts that + // say "report to X" or "no report needed". Mirrors the CLI launch path. + fn append_reply_handoff_for( + tool: &str, + fork: bool, + is_adoption: bool, + child_name: Option<&str>, + ) -> bool { + let (_system, _initial, append) = build_resume_prompts(ResumePromptInput { + tool, + display_name: "luna", + fork, + is_adoption, + child_name, + effective_tag: None, + custom_system_prompt: None, + custom_initial_prompt: None, + }); + append + } + + #[test] + fn test_resume_never_appends_reply_handoff() { + // Plain tracked resume (claude / codex). + assert!(!append_reply_handoff_for("claude", false, false, None)); + assert!(!append_reply_handoff_for("codex", false, false, None)); + // Claude tracked fork (hcom f) — previously still appended the footer. + assert!(!append_reply_handoff_for( + "claude", + true, + false, + Some("nova") + )); + // Codex tracked fork — already suppressed before; stays suppressed. + assert!(!append_reply_handoff_for( + "codex", + true, + false, + Some("nova") + )); + // Adoption resume / fork (on-disk session, no prior hcom identity). + assert!(!append_reply_handoff_for("claude", false, true, None)); + assert!(!append_reply_handoff_for("claude", true, true, None)); + } + #[test] fn test_build_resume_args_codex_fork() { let args = build_resume_args("codex", "sess-456", true);