Skip to content
Open
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
5 changes: 4 additions & 1 deletion src/commands/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result<i32> {
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,
},
)?;

Expand Down
64 changes: 58 additions & 6 deletions src/commands/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -846,9 +845,15 @@ fn build_resume_prompts(input: ResumePromptInput<'_>) -> (Option<String>, 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 @<launcher>" 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)
}

Expand Down Expand Up @@ -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 @<launcher>" 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);
Expand Down
36 changes: 28 additions & 8 deletions src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1737,15 +1737,10 @@ pub fn launch(db: &HcomDb, mut params: LaunchParams) -> Result<LaunchResult> {
});

// 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)?;
}
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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"), "");
}
}
Loading