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
44 changes: 37 additions & 7 deletions crates/daemon/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,14 +632,26 @@ fn program_execution_prompt_with_comment(comment: Option<&str>) -> String {
prompt
}

fn forked_program_execution_prompt(owner_session_id: &str, comment: Option<&str>) -> String {
fn forked_program_execution_prompt(
owner_session_id: &str,
fork_session_id: &str,
comment: Option<&str>,
) -> String {
let mut prompt = program_execution_prompt_with_comment(comment);
prompt.push_str(&format!(
"\n\nYou are running in an interactive fork. The Program you must read and update belongs \
to session `{owner_session_id}`. Always pass `session_id: \"{owner_session_id}\"` to \
construct_program_get and every construct_program_edit/update call; do not edit a \
Program belonging to this fork. Apply progress and results directly to that owner \
document as you work. Do not return a result for the owner to merge."
"\n\nYou are running in an interactive fork (session `{fork_session_id}`). The Program \
you must read and update belongs to session `{owner_session_id}`. Always pass \
`session_id: \"{owner_session_id}\"` to construct_program_get and every \
construct_program_edit/update call; do not edit a Program belonging to this fork. Apply \
progress and results directly to that owner document as you work. Do not return a \
result for the owner to merge.\
\n\nWhen the dispatched work is fully complete and you have settled every block you \
were dispatched for on the owner Program, close this fork as your final action by \
calling the archive-session tool (construct_archive_session, or agentd_archive_session \
if you have the agentd-prefixed toolset) with `session_id: \"{fork_session_id}\"`. \
Archiving is a soft close: the transcript and the owner Program's session clip stay \
valid. If work remains pending, you are blocked, or the user has joined the \
conversation in this fork, leave the session open instead of archiving."
));
prompt
}
Expand Down Expand Up @@ -2858,7 +2870,7 @@ impl SessionManager {
// The fork's own context env points at this sidecar. Store the
// owner's Program context there before delivering the first turn.
self.write_program_run_context(&fork_id, &run_context)?;
let prompt = forked_program_execution_prompt(&params.session_id, run_comment);
let prompt = forked_program_execution_prompt(&params.session_id, &fork_id, run_comment);
self.spawn_program_fork_prompt_delivery(
fork_id.clone(),
fork_created_at_ms,
Expand Down Expand Up @@ -5655,6 +5667,24 @@ mod tests {
assert!(!prompt.contains("focus tests\nand keep output short"));
}

/// Spec 0076 auto-close contract: a Run fork's prompt tells it to
/// archive ITSELF (its own session id, not the owner's) as its final
/// action once every dispatched block has settled, and to stay open
/// when blocked / work remains / the user joined the conversation.
/// The owner-targeting contract must survive alongside it.
#[test]
fn forked_run_prompt_instructs_self_archive_on_completion() {
let prompt = forked_program_execution_prompt("owner1", "fork1", None);

assert!(prompt.contains("session_id: \"owner1\""));
assert!(prompt.contains("do not edit a Program belonging to this fork"));
assert!(prompt.contains("construct_archive_session"));
assert!(prompt.contains("session_id: \"fork1\""));
assert!(prompt.contains("settled every block"));
assert!(prompt.contains("final action"));
assert!(prompt.contains("leave the session open"));
}

#[test]
fn program_run_context_carries_run_contract_and_registered_smart_clips() {
let program = ProgramDocument {
Expand Down
2 changes: 2 additions & 0 deletions specs/0076-program-selection-run-comment.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Selection Run executes in a visible interactive same-harness fork by default, ev

When the Run is dispatched to a fork, the daemon annotates the selection with the fork's session clip at dispatch time — the same convention selection verbs and instant dispatch follow — so the Program document shows where the work went and renders the fork's live state in place. The annotated selection keeps its pending shimmer with a plain "Running" status. The annotation is best-effort: if the selection anchor no longer applies verbatim (concurrent edit, ambiguity), the Run proceeds without a clip rather than failing. Owner-targeted Runs (Shift, or callers not requesting a fork) add no session clip: the work happens in the session the user is already looking at, and a self-referential clip would be noise.

A Run fork auto-closes when its task is complete. The fork's dispatch prompt instructs it to archive itself — a soft close that keeps the transcript and the Program's session clip valid — as its final action, only after every block it was dispatched for has settled on the owner Program, and to stay open when work remains pending, it is blocked, or the user has joined the conversation in the fork. This contract is prompt-enforced (the fork performs the archive with its session tools); a daemon-side deterministic close may later back it up, and must preserve the same completion condition and stay-open exceptions. Owner-targeted Runs never auto-close anything: the owning session is the user's own workspace.

The focused instruction editor supports the same basic single-line movement and deletion keys users expect elsewhere in the TUI: C-a, C-e, C-b, C-f, C-d, and C-k. Because the field can wrap visually, C-p, C-n, Up, and Down move between wrapped visual rows. Its Run button remains visually distinct from typed text, is aligned to the right edge, and is highlighted only while the context menu is focused or hovered. The context menu content has one-column horizontal padding inside the border.

The extra instruction is run metadata, not Program content. It must not alter the selected markdown, selection block identity, or optimistic shimmer scope. The daemon appends the instruction to the generated Program Run prompt and disables mechanical fast paths that cannot interpret it.
Expand Down
Loading