Skip to content

Commit f534aeb

Browse files
committed
feat: Preserve compact history and refine desktop UI
1 parent d1da9e9 commit f534aeb

47 files changed

Lines changed: 2508 additions & 542 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/daemon-api.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,12 @@ Errors:
307307
`/init` with a provider enqueues the same init prompt used by the TUI while
308308
displaying `/init` in the transcript. Without a provider it writes the offline
309309
`ACECODE.md` skeleton and emits a visible system message. `/compact` runs on the
310-
AgentLoop worker queue, emits progress/completion/error messages, rewrites the
311-
active JSONL transcript on success, and emits a `transcript_replace` event with
312-
the compacted message list plus `messages_compressed` and
313-
`estimated_tokens_saved`.
310+
AgentLoop worker queue and emits progress/completion/error messages. On success
311+
it appends a hidden compact checkpoint to the session JSONL plus visible system
312+
marker messages; older user-visible transcript rows remain available in history.
313+
The checkpoint carries the provider-facing replacement history used for later
314+
model requests and resume/fork reconstruction. Normal manual, auto, and rescue
315+
compact success does not emit `transcript_replace`.
314316

315317
### `GET /api/sessions/:id/permissions`
316318

@@ -537,7 +539,7 @@ server : unsubscribes from EventDispatcher; AgentLoop keeps running
537539
| `ToolEnd` | Tool finished | `{ "tool": "...", "ok": true, "summary": ToolSummary?, "hunks": ToolHunks?, "output_tail": "..." }` |
538540
| `PermissionRequest`| Tool needs user confirmation | `{ "request_id": "...", "tool": "...", "args": {...}, "options": ["allow","deny","allow_session"] }` |
539541
| `Usage` | LLM reported token usage | `{ "prompt_tokens": N, "completion_tokens": N, "total_tokens": N }` |
540-
| `TranscriptReplace`| `/compact` replaced the visible transcript | `{ "messages": ChatMessage[], "messages_compressed": N, "estimated_tokens_saved": N }` |
542+
| `TranscriptReplace`| The server must replace the visible transcript for recovery/cleanup, such as retry or partial-stream cleanup; normal compact success does not use this event | `{ "messages": ChatMessage[] }` plus cleanup-specific fields |
541543
| `BusyChanged` | Transition between idle / waiting / running | `{ "busy": true, "reason": "waiting_llm"|"running_tool" }` |
542544
| `Done` | Agent loop reached a terminator (text reply / `task_complete` / max_iterations / abort) | `{ "reason": "text"|"task_complete"|"abort"|"max_iters", "summary": "..."? }` |
543545
| `Error` | Something failed (provider error / tool exception / permission timeout) | `{ "code": "...", "message": "..." }` |

main.cpp

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4680,25 +4680,8 @@ static int run_interactive_app(const InteractiveCliOptions& cli,
46804680
return;
46814681
}
46824682
std::lock_guard<std::mutex> lk(state.mu);
4683-
int tui_keep = 0;
4684-
int tui_turns = 0;
4685-
for (int i = static_cast<int>(state.conversation.size()) - 1; i >= 0; --i) {
4686-
if (state.conversation[i].role == "user") {
4687-
tui_turns++;
4688-
if (tui_turns >= 4) {
4689-
tui_keep = i;
4690-
break;
4691-
}
4692-
}
4693-
}
4694-
std::vector<TuiState::Message> new_conv;
4695-
new_conv.reserve(state.conversation.size() - tui_keep + 2);
4696-
new_conv.push_back({"system", "--- [Compact Boundary] ---", false});
4697-
new_conv.push_back({"system", "[Conversation summary]\n" + result.summary_text, false});
4698-
new_conv.insert(new_conv.end(),
4699-
state.conversation.begin() + tui_keep,
4700-
state.conversation.end());
4701-
state.conversation = std::move(new_conv);
4683+
state.conversation.push_back({"system", "--- [Compact Checkpoint] ---", false});
4684+
state.conversation.push_back({"system", "[Conversation summary]\n" + result.summary_text, false});
47024685
reset_chat_line_measure_state();
47034686
state.chat_follow_tail = true;
47044687
clamp_chat_focus();

src/agent_loop.cpp

Lines changed: 58 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "utils/text_file_buffer.hpp"
99
#include "commands/compact.hpp"
1010
#include "commands/micro_compact.hpp"
11+
#include "session/compact_checkpoint.hpp"
1112
#include "session/tool_metadata_codec.hpp"
1213
#include "session/tool_result_storage.hpp"
1314
#include "session/output_attachments.hpp"
@@ -267,6 +268,11 @@ nlohmann::json build_transcript_replace_payload(
267268
const CompactResult& result) {
268269
nlohmann::json arr = nlohmann::json::array();
269270
for (const auto& msg : messages) {
271+
if (is_file_checkpoint_message(msg)) continue;
272+
if (is_compact_checkpoint_message(msg)) continue;
273+
if (is_content_replacement_message(msg)) continue;
274+
if (is_turn_timing_message(msg)) continue;
275+
if (web::is_hidden_goal_context_message(msg)) continue;
270276
arr.push_back(web::chat_message_to_payload_json(msg));
271277
}
272278
return nlohmann::json{
@@ -661,24 +667,29 @@ void AgentLoop::emit_transcript_system_message(const std::string& content,
661667
}
662668

663669
bool AgentLoop::active_estimate_exceeds_auto_threshold() const {
664-
auto [start, count] = get_messages_after_compact_boundary(messages_);
665-
std::vector<ChatMessage> active(messages_.begin() + start,
666-
messages_.begin() + start + count);
667-
return estimate_message_tokens(active) > get_auto_compact_threshold(context_window_);
670+
return estimate_message_tokens(provider_relevant_messages(messages_)) >
671+
get_auto_compact_threshold(context_window_);
668672
}
669673

670-
void AgentLoop::apply_compact_result(const CompactResult& result) {
671-
messages_ = result.compacted_messages;
674+
void AgentLoop::apply_compact_result(const CompactResult& result, const std::string& trigger) {
675+
const int pre_tokens = estimate_message_tokens(provider_relevant_messages(messages_));
676+
std::vector<ChatMessage> replacement_history =
677+
provider_relevant_messages(result.compacted_messages);
678+
const int post_tokens = estimate_message_tokens(replacement_history);
679+
672680
last_api_prompt_tokens_.store(0);
673681
if (session_manager_) {
674-
session_manager_->replace_active_messages(messages_);
675-
}
676-
677-
events_.emit(SessionEventKind::TranscriptReplace,
678-
build_transcript_replace_payload(messages_, result));
679-
if (callbacks_.on_transcript_replace) {
680-
callbacks_.on_transcript_replace(messages_, result);
682+
CompactCheckpoint checkpoint;
683+
checkpoint.trigger = trigger;
684+
checkpoint.summary = result.summary_text;
685+
checkpoint.messages_compressed = result.messages_compressed;
686+
checkpoint.estimated_tokens_saved = result.estimated_tokens_saved;
687+
checkpoint.pre_tokens = pre_tokens;
688+
checkpoint.post_tokens = post_tokens;
689+
checkpoint.replacement_history = replacement_history;
690+
session_manager_->append_compact_checkpoint(checkpoint);
681691
}
692+
messages_ = std::move(replacement_history);
682693
}
683694

684695
bool AgentLoop::maybe_run_auto_compact() {
@@ -690,16 +701,15 @@ bool AgentLoop::maybe_run_auto_compact() {
690701
" context_window=" + std::to_string(context_window_) +
691702
" last_api_prompt_tokens=" +
692703
std::to_string(last_api_prompt_tokens_.load()));
693-
dispatch_message("system",
694-
"[Auto-compact] Skipped after repeated compaction failures.",
695-
false);
704+
emit_transcript_system_message(
705+
"[Auto-compact] Skipped after repeated compaction failures.");
696706
return false;
697707
}
698708

699-
auto [boundary_start, boundary_count] = get_messages_after_compact_boundary(messages_);
700-
(void)boundary_count;
701-
int pre_tokens = estimate_message_tokens(
702-
std::vector<ChatMessage>(messages_.begin() + boundary_start, messages_.end()));
709+
const int boundary_start = 0;
710+
const auto active_for_estimate = provider_relevant_messages(messages_);
711+
const int boundary_count = static_cast<int>(active_for_estimate.size());
712+
int pre_tokens = estimate_message_tokens(active_for_estimate);
703713
const int threshold = get_auto_compact_threshold(context_window_);
704714
LOG_INFO("Auto-compact preflight; messages=" + std::to_string(messages_.size()) +
705715
" active_start=" + std::to_string(boundary_start) +
@@ -718,9 +728,7 @@ bool AgentLoop::maybe_run_auto_compact() {
718728
auto outcome = dispatch_codex_hook(kCodexHookEventPreCompact, "auto", payload);
719729
apply_hook_side_effects(outcome);
720730
if (outcome.continue_false || outcome.blocked || outcome.denied) {
721-
dispatch_message("system",
722-
"[Auto-compact] Stopped by hook.",
723-
false);
731+
emit_transcript_system_message("[Auto-compact] Stopped by hook.");
724732
return false;
725733
}
726734
}
@@ -732,30 +740,22 @@ bool AgentLoop::maybe_run_auto_compact() {
732740
micro_result.estimated_tokens_saved,
733741
micro_result.cleared_tool_call_ids));
734742
last_api_prompt_tokens_.store(0);
735-
if (session_manager_) {
736-
session_manager_->replace_active_messages(messages_);
737-
}
738-
739743
CompactResult replace_result;
740744
replace_result.performed = true;
745+
replace_result.messages_compressed = micro_result.tool_results_cleared;
741746
replace_result.estimated_tokens_saved = micro_result.estimated_tokens_saved;
747+
replace_result.summary_text = "[Micro-compact] Cleared old tool results.";
742748
replace_result.compacted_messages = messages_;
743-
events_.emit(SessionEventKind::TranscriptReplace,
744-
build_transcript_replace_payload(messages_, replace_result));
749+
apply_compact_result(replace_result, "auto");
745750

746-
dispatch_message(
747-
"system",
751+
emit_transcript_system_message(
748752
"[Micro-compact] Cleared " +
749753
std::to_string(micro_result.tool_results_cleared) +
750754
" old tool results, saved ~" +
751755
TokenTracker::format_tokens(micro_result.estimated_tokens_saved) +
752-
" tokens",
753-
false);
756+
" tokens");
754757

755-
auto [after_start, after_count] = get_messages_after_compact_boundary(messages_);
756-
std::vector<ChatMessage> active_after(messages_.begin() + after_start,
757-
messages_.begin() + after_start + after_count);
758-
const int after_tokens = estimate_message_tokens(active_after);
758+
const int after_tokens = estimate_message_tokens(provider_relevant_messages(messages_));
759759
const bool still_above_threshold = after_tokens > threshold;
760760
LOG_INFO("Auto micro-compact result; cleared_tool_results=" +
761761
std::to_string(micro_result.tool_results_cleared) +
@@ -789,9 +789,8 @@ bool AgentLoop::maybe_run_auto_compact() {
789789
{"label", "Compacting conversation"},
790790
{"started_at_ms", now_epoch_ms()},
791791
});
792-
dispatch_message("system",
793-
"[Auto-compact] Context approaching limit, compacting...",
794-
false);
792+
emit_transcript_system_message(
793+
"[Auto-compact] Context approaching limit, compacting...");
795794

796795
std::shared_ptr<LlmProvider> provider_snapshot;
797796
if (provider_accessor_) provider_snapshot = provider_accessor_();
@@ -802,9 +801,8 @@ bool AgentLoop::maybe_run_auto_compact() {
802801
std::to_string(auto_compact_consecutive_failures_) +
803802
" active_estimated_tokens=" + std::to_string(pre_tokens) +
804803
" threshold=" + std::to_string(threshold));
805-
dispatch_message("system",
806-
"[Auto-compact] provider unavailable for compaction",
807-
false);
804+
emit_transcript_system_message(
805+
"[Auto-compact] provider unavailable for compaction");
808806
return false;
809807
}
810808

@@ -826,7 +824,7 @@ bool AgentLoop::maybe_run_auto_compact() {
826824
" error=" + log_truncate(result.error, 500) +
827825
" active_estimated_tokens=" + std::to_string(pre_tokens) +
828826
" threshold=" + std::to_string(threshold));
829-
dispatch_message("system", "[Auto-compact] " + result.error, false);
827+
emit_transcript_system_message("[Auto-compact] " + result.error);
830828
return false;
831829
}
832830

@@ -839,7 +837,7 @@ bool AgentLoop::maybe_run_auto_compact() {
839837
" estimated_tokens_saved=" +
840838
std::to_string(result.estimated_tokens_saved) +
841839
" compacted_estimated_tokens=" + std::to_string(compacted_tokens));
842-
apply_compact_result(result);
840+
apply_compact_result(result, "auto");
843841
if (hook_manager_) {
844842
auto fields = build_hook_common_fields(kCodexHookEventPostCompact);
845843
auto payload = build_compact_hook_payload(fields, "auto");
@@ -849,12 +847,10 @@ bool AgentLoop::maybe_run_auto_compact() {
849847
}
850848
auto_compact_consecutive_failures_ = 0;
851849

852-
dispatch_message(
853-
"system",
850+
emit_transcript_system_message(
854851
"[Auto-compact] Compacted " + std::to_string(result.messages_compressed) +
855852
" messages, saved ~" +
856-
TokenTracker::format_tokens(result.estimated_tokens_saved) + " tokens",
857-
false);
853+
TokenTracker::format_tokens(result.estimated_tokens_saved) + " tokens");
858854
return true;
859855
}
860856

@@ -1182,8 +1178,8 @@ AgentLoop::ApiRequestBundle AgentLoop::build_api_request_messages() {
11821178
bundle.tool_defs = tools_.get_tool_definitions();
11831179
LOG_DEBUG("Registered tools: " + std::to_string(bundle.tool_defs.size()));
11841180

1185-
// Prepare messages with system prompt at front, filtering out meta messages
1186-
auto api_messages = normalize_messages_for_api(messages_);
1181+
// Prepare provider-facing messages with system prompt at front.
1182+
auto api_messages = provider_relevant_messages(messages_);
11871183
std::string session_context = cached_context_for_api(
11881184
build_session_context_prompt(
11891185
cwd_, memory_registry_, memory_cfg_, project_instructions_cfg_,
@@ -1329,8 +1325,10 @@ AgentLoop::ProviderCallResult AgentLoop::call_provider_and_collect(
13291325
callbacks_.on_stream_retry_reset();
13301326
}
13311327
CompactResult reset_result;
1328+
std::vector<ChatMessage> visible_reset_messages =
1329+
session_manager_ ? session_manager_->load_active_messages() : messages_;
13321330
events_.emit(SessionEventKind::TranscriptReplace,
1333-
build_transcript_replace_payload(messages_, reset_result));
1331+
build_transcript_replace_payload(visible_reset_messages, reset_result));
13341332
}
13351333
emit_progress(
13361334
"model_retry",
@@ -1447,7 +1445,7 @@ AgentLoop::HandleErrorResult AgentLoop::handle_provider_error(
14471445
std::max(0, rescue.estimated_tokens_before - rescue.estimated_tokens_after);
14481446
replace_result.summary_text = rescue.marker_text;
14491447
replace_result.compacted_messages = std::move(rescue.compacted_messages);
1450-
apply_compact_result(replace_result);
1448+
apply_compact_result(replace_result, "rescue");
14511449

14521450
++context_rescue_attempts;
14531451
last_context_rescue_tokens = rescue.estimated_tokens_after;
@@ -1469,14 +1467,12 @@ AgentLoop::HandleErrorResult AgentLoop::handle_provider_error(
14691467
std::to_string(rescue.estimated_tokens_after) +
14701468
" request_estimated_tokens_before=" +
14711469
std::to_string(request_tokens));
1472-
dispatch_message(
1473-
"system",
1470+
emit_transcript_system_message(
14741471
"[Rescue compact] Provider rejected the request as too large; compacted " +
14751472
std::to_string(rescue.messages_removed) +
14761473
" messages and retrying with the most recent " +
14771474
std::to_string(rescue.protected_user_turns) +
1478-
" user turn(s).",
1479-
false);
1475+
" user turn(s).");
14801476
return HandleErrorResult::Continue;
14811477
}
14821478

@@ -2575,7 +2571,7 @@ void AgentLoop::run_compact() {
25752571
{"label", "Compacting conversation"},
25762572
{"started_at_ms", now_epoch_ms()},
25772573
});
2578-
dispatch_message("system", "Compacting conversation...", false);
2574+
emit_transcript_system_message("Compacting conversation...");
25792575

25802576
auto finish = [this]() {
25812577
if (callbacks_.on_busy_changed) {
@@ -2592,7 +2588,7 @@ void AgentLoop::run_compact() {
25922588
auto outcome = dispatch_codex_hook(kCodexHookEventPreCompact, "manual", payload);
25932589
apply_hook_side_effects(outcome);
25942590
if (outcome.continue_false || outcome.blocked || outcome.denied) {
2595-
dispatch_message("system", "[Compact] Stopped by hook.", false);
2591+
emit_transcript_system_message("[Compact] Stopped by hook.");
25962592
finish();
25972593
return;
25982594
}
@@ -2620,7 +2616,7 @@ void AgentLoop::run_compact() {
26202616
return;
26212617
}
26222618

2623-
apply_compact_result(result);
2619+
apply_compact_result(result, "manual");
26242620
if (hook_manager_) {
26252621
auto fields = build_hook_common_fields(kCodexHookEventPostCompact);
26262622
auto payload = build_compact_hook_payload(fields, "manual");
@@ -2632,12 +2628,10 @@ void AgentLoop::run_compact() {
26322628
}
26332629
}
26342630

2635-
dispatch_message(
2636-
"system",
2631+
emit_transcript_system_message(
26372632
"Compacted " + std::to_string(result.messages_compressed) +
26382633
" messages, saved ~" +
2639-
std::to_string(result.estimated_tokens_saved) + " tokens.",
2640-
false);
2634+
std::to_string(result.estimated_tokens_saved) + " tokens.");
26412635
finish();
26422636
}
26432637

src/agent_loop.hpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ struct AgentCallbacks {
7070
// The payload shape matches the todo_updated session event.
7171
std::function<void(const nlohmann::json& payload)> on_todo_updated;
7272

73-
// Called after the shared AgentLoop compact path has replaced model
74-
// history. TUI uses this as a display observer; daemon/Web consumers use
75-
// the TranscriptReplace event stream instead.
73+
// Legacy display observer for replacement-style transcript updates. Normal
74+
// compact success appends marker messages and no longer calls this hook.
7675
std::function<void(const std::vector<ChatMessage>& messages,
7776
const CompactResult& result)> on_transcript_replace;
7877

@@ -250,7 +249,7 @@ class AgentLoop {
250249
std::string build_goal_context_prompt(const ThreadGoal& goal) const;
251250
bool maybe_run_auto_compact();
252251
bool active_estimate_exceeds_auto_threshold() const;
253-
void apply_compact_result(const CompactResult& result);
252+
void apply_compact_result(const CompactResult& result, const std::string& trigger);
254253

255254
// Section 7: 同时调老 on_message callback(若 TUI 挂了)和新事件流
256255
// (events_)。所有 on_message 触发点都该走这个 helper,确保 daemon

0 commit comments

Comments
 (0)