Persist explicit user goal edits in model history (#49598)

## Why

User goal edits and clears need to reach the model as user instructions, including when a thread is unloaded or history is being compacted. Automatic goal lifecycle updates must not supply user authorization.

## What changed

- Add optional `origin` values of `user` and `automatic` to `thread/goal/set` and `thread/goal/clear`, and mark TUI goal requests as user actions. Omitted origins do not supply user authorization.
- Persist explicit user objective, status, and clear instructions before changing goal state. Recording failures leave goal state unchanged.
- Coordinate goal edits with thread resume, reload stored history under writer ownership, and preserve edits accepted after the compaction input snapshot.
- Update generated protocol schemas and TypeScript and Python bindings.

## Testing

Add coverage for user edits and clears on loaded and unloaded threads, concurrent path-based resume, exclusion of automatic goals from user instructions, preservation of edits during compaction, and recording failures that leave goal state unchanged.

GitOrigin-RevId: d779d7c678a0c32a6ef76975dbe5b1400d1df245
This commit is contained in:
felixxia-oai
2026-09-30 11:58:03 +00:00
committed by copyberry
parent 1fc8d54807
commit de02016798
35 changed files with 698 additions and 109 deletions
@@ -3749,6 +3749,17 @@ impl ThreadRequestProcessor {
}
};
let (thread_history, resume_source_thread) = resume_result?;
// Path-based resume can use an empty request thread ID. Coordinate once its real
// identity is known; unrelated loaded threads never wait for this cold startup.
let _goal_resume_guard = if let InitialHistory::Resumed(resumed) = &thread_history {
Some(
self.thread_state_manager
.lock_goal_resume(resumed.conversation_id)
.await,
)
} else {
None
};
if let InitialHistory::Resumed(resumed) = &thread_history
&& self
.pending_thread_unloads
@@ -3948,7 +3959,8 @@ impl ThreadRequestProcessor {
let mut config = match prepared_config.take() {
Some(prepared) if prepared.state == config_state => prepared.config,
_ => {
// Config loading can call back into Desktop; release the permit during host work.
// Config loading can call back into Desktop; release both locks during host work.
drop(_goal_resume_guard);
drop(_thread_list_state_permit);
let config = self
.config_manager