mirror of
https://github.com/openai/codex.git
synced 2026-11-03 08:22:30 +00:00
Move message history out of core (#21278)
## Why Message history was implemented inside `codex-core` and surfaced through core protocol ops and `SessionConfiguredEvent` fields even though the current consumer is TUI-local prompt recall. That made core own UI history persistence and exposed `history_log_id` / `history_entry_count` through surfaces that app-server and other clients do not need. This change moves message history persistence out of core and keeps the recall plumbing local to the TUI. ## What changed - Added a new `codex-message-history` crate for appending, looking up, trimming, and reading metadata from `history.jsonl`. - Removed core protocol history ops/events: `AddToHistory`, `GetHistoryEntryRequest`, and `GetHistoryEntryResponse`. - Removed `history_log_id` and `history_entry_count` from `SessionConfiguredEvent` and updated exec/MCP/test fixtures accordingly. - Updated the TUI to dispatch local app events for message-history append/lookup and keep its persistent-history metadata in TUI session state. ## Validation - `cargo test -p codex-message-history -p codex-protocol` - `cargo test -p codex-exec event_processor_with_json_output` - `cargo test -p codex-mcp-server outgoing_message` - `cargo test -p codex-tui` - `just fix -p codex-message-history -p codex-protocol -p codex-core -p codex-tui -p codex-exec -p codex-mcp-server`
This commit is contained in:
1 parent
be1d3cff93
commit
2004173cd7
45 files changed
+375
-492
No files matched your search
@@ -29,8 +29,7 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -133,8 +132,7 @@ async fn submission_includes_configured_permission_profile() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -183,8 +181,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -225,8 +222,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -319,8 +315,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -383,8 +378,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -422,8 +416,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -461,8 +454,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -503,8 +495,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
@@ -958,8 +958,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -977,8 +976,8 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
other => panic!("expected RunUserShellCommand op, got {other:?}"),
|
||||
}
|
||||
assert_matches!(
|
||||
op_rx.try_recv(),
|
||||
Ok(Op::AddToHistory { text }) if text == "!echo hi"
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::AppendMessageHistoryEntry { text, .. }) if text == "!echo hi"
|
||||
);
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ async fn resumed_initial_messages_render_history() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -102,8 +101,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -171,8 +169,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -271,8 +268,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
cwd: expected_cwd.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
@@ -328,8 +324,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
cwd: test_path_buf("/home/user/external").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
@@ -367,8 +362,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -422,8 +416,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -693,8 +686,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
@@ -739,8 +731,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
|
||||
@@ -586,8 +586,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
});
|
||||
@@ -634,8 +633,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
|
||||
cwd,
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
});
|
||||
|
||||
@@ -1219,8 +1219,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
@@ -1463,8 +1462,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
|
||||
@@ -38,14 +38,16 @@ fn recall_latest_after_clearing(chat: &mut ChatWidget) -> String {
|
||||
chat.bottom_pane.composer_text()
|
||||
}
|
||||
|
||||
fn next_add_to_history_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> String {
|
||||
fn next_add_to_history_event(rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>) -> String {
|
||||
loop {
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::AddToHistory { text }) => return text,
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::AppendMessageHistoryEntry { text, .. }) => return text,
|
||||
Ok(_) => continue,
|
||||
Err(TryRecvError::Empty) => panic!("expected AddToHistory op but queue was empty"),
|
||||
Err(TryRecvError::Empty) => {
|
||||
panic!("expected AppendMessageHistoryEntry event but queue was empty")
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
panic!("expected AddToHistory op but channel closed")
|
||||
panic!("expected AppendMessageHistoryEntry event but channel closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,15 +118,6 @@ async fn queued_slash_review_with_args_dispatches_after_active_turn() {
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::AddToHistory { .. }) => match op_rx.try_recv() {
|
||||
Ok(Op::Review { target }) => assert_eq!(
|
||||
target,
|
||||
ReviewTarget::Custom {
|
||||
instructions: "check regressions".to_string(),
|
||||
}
|
||||
),
|
||||
other => panic!("expected queued /review to submit review op, got {other:?}"),
|
||||
},
|
||||
Ok(Op::Review { target }) => assert_eq!(
|
||||
target,
|
||||
ReviewTarget::Custom {
|
||||
@@ -152,7 +145,7 @@ async fn queued_slash_review_with_args_restores_for_edit() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_bang_shell_dispatches_after_active_turn() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
@@ -171,10 +164,7 @@ async fn queued_bang_shell_dispatches_after_active_turn() {
|
||||
Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),
|
||||
other => panic!("expected queued shell command op, got {other:?}"),
|
||||
}
|
||||
assert_matches!(
|
||||
op_rx.try_recv(),
|
||||
Ok(Op::AddToHistory { text }) if text == "!echo hi"
|
||||
);
|
||||
assert_eq!(next_add_to_history_event(&mut rx), "!echo hi");
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
}
|
||||
|
||||
@@ -217,7 +207,7 @@ async fn queued_empty_bang_shell_reports_help_when_dequeued_and_drains_next_inpu
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
@@ -230,10 +220,7 @@ async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() {
|
||||
Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),
|
||||
other => panic!("expected queued shell command op, got {other:?}"),
|
||||
}
|
||||
assert_matches!(
|
||||
op_rx.try_recv(),
|
||||
Ok(Op::AddToHistory { text }) if text == "!echo hi"
|
||||
);
|
||||
assert_eq!(next_add_to_history_event(&mut rx), "!echo hi");
|
||||
assert_eq!(chat.queued_user_messages.len(), 1);
|
||||
|
||||
let begin = begin_exec_with_source(
|
||||
@@ -412,10 +399,10 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() {
|
||||
),
|
||||
other => panic!("expected first queued message after /rename, got {other:?}"),
|
||||
}
|
||||
assert_matches!(
|
||||
op_rx.try_recv(),
|
||||
Ok(Op::AddToHistory { text }) if text == "first after rename"
|
||||
);
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::AppendMessageHistoryEntry { text, .. } if text == "first after rename"
|
||||
)));
|
||||
assert_eq!(
|
||||
chat.queued_user_message_texts(),
|
||||
vec!["second after rename"]
|
||||
@@ -945,7 +932,7 @@ fn merged_history_record_remaps_override_image_placeholders() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupted_merged_message_history_encodes_mentions_once() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.on_agent_message_delta("Final answer line\n".to_string());
|
||||
@@ -977,7 +964,7 @@ async fn interrupted_merged_message_history_encodes_mentions_once() {
|
||||
other => panic!("expected user turn, got {other:?}"),
|
||||
}
|
||||
let encoded = "use [$figma](app://figma) now";
|
||||
assert_eq!(next_add_to_history_op(&mut op_rx), encoded);
|
||||
assert_eq!(next_add_to_history_event(&mut rx), encoded);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
next_interrupt_op(&mut op_rx);
|
||||
@@ -997,7 +984,7 @@ async fn interrupted_merged_message_history_encodes_mentions_once() {
|
||||
}
|
||||
other => panic!("expected resubmitted user turn, got {other:?}"),
|
||||
}
|
||||
assert_eq!(next_add_to_history_op(&mut op_rx), encoded);
|
||||
assert_eq!(next_add_to_history_event(&mut rx), encoded);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1863,8 +1863,7 @@ async fn session_configured_clears_goal_status_footer() {
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user