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
1 change: 1 addition & 0 deletions openspec/changes/persist-session-error-notices/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ See proposal.md for the incident. Agent error messages use `TranscriptWriter::di
3. Distinguish durable errors by `role=error`, `metadata.transcript_only=true`, and a nonempty ID. Reconcile these by ID in both replay paths; bypass content-only deduplication for different error IDs. Unmarked legacy errors retain occurrence/sequence semantics.
4. Reuse the existing provider-history filter, and test both the actual next provider request and resumed history. No UI style changes or additional context protocol is required.
5. When a storage failure causes an error notice, retain that original storage diagnostic while attempting to save the notice. Failed compaction preserves the previous history prefix and context window; its new transcript-only error is visible without pretending the failed checkpoint was stored.
6. Keep the public checked-append lock and diagnostic guard in the existing session history implementation. Delegate the actual append to a private helper requiring that lock, keeping the main session manager within its established file-size limit without changing append behavior.

## Risks / Trade-offs

Expand Down
1 change: 1 addition & 0 deletions openspec/changes/persist-session-error-notices/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
- [x] 2.1 Document the additive daemon message contract and verify strict OpenSpec validation plus diff checks.
- [x] 2.2 Run relevant Windows native regression tests, full Web tests, Web build, and a browser check of restored red error cards; record results and runtime limitations.
- [x] 2.3 Preserve the original storage failure when persisting its error notice also fails, update blocked-input/failed-compaction expectations for transcript-only errors, and pass focused plus full Windows release regressions.
- [x] 2.4 Keep the checked-append facade in the existing history implementation to satisfy the file-size limit; verify all strict gates individually and rerun native session regressions after the move.
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ The browser check uses isolated simulated responses, not the affected remote ser
- Full Windows regression exposed two old assumptions that failures leave no visible error record, plus a diagnostic regression when saving the notice also failed. Retained the original storage error under the session lock, and updated blocked-input and failed-compaction assertions to require transcript-only errors while preserving the original history prefix and context window.
- Focused release regressions: 41 passed, zero failures.
- Final full Windows regression: 6239 inventory entries, 6238 executed, 6229 passed, 9 skipped, zero failures; one disabled benchmark. Ran all suites with `run_fast_tests.py --profile full --shards 6` and isolated profiles. Report: `C:/Users/shao/AppData/Local/Temp/acecode-release-1.0.3/windows-full-final.json`.
- The CI file-size gate then rejected eight added lines in the main session manager. Moved the public checked-append lock/diagnostic facade into the existing history implementation and retained a private locked append helper. All seven strict gates passed individually with fail-fast handling, including the cloud layer gate. Rebuilt Windows tests and passed 91 session, storage, history, hook, reasoning, and payload regressions after this structural move (`final-layout-tests.json`).
11 changes: 1 addition & 10 deletions src/domain/session/session_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#include "session_usage_ledger.hpp"
#include "utils/atomic_file.hpp"
#include "utils/logger.hpp"
#include "utils/scope_exit.hpp"
#include "utils/uuid.hpp"
#include "utils/utf8_path.hpp"

Expand Down Expand Up @@ -340,15 +339,7 @@ bool SessionManager::ensure_created() {
return true;
}

bool SessionManager::try_on_message(const ChatMessage& msg) {
std::lock_guard<std::mutex> lk(mu_);
// Recording an error notice must not replace the storage failure that
// caused it, even when that notice also cannot be written to disk.
std::string original_error = msg.role == "error" && msg.metadata.is_object() &&
msg.metadata.value("transcript_only", false) ? last_error_ : std::string{};
ScopeExit preserve_error([this, original_error = std::move(original_error)]() mutable {
if (!original_error.empty()) last_error_.swap(original_error);
});
bool SessionManager::try_on_message_locked(const ChatMessage& msg) {
last_error_.clear();
if (!started_ || !ensure_created()) return false;

Expand Down
1 change: 1 addition & 0 deletions src/domain/session/session_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ class SessionManager {
std::string current_trajectory_path() const;

private:
bool try_on_message_locked(const ChatMessage& msg); // Requires mu_.
void record_recent_activity_locked(const ChatMessage& msg);
void restore_recent_activity_locked(const SessionMeta& persisted_meta);
void publish_display_snapshot_locked(); // mu_ held; display_mu_ is a leaf lock.
Expand Down
13 changes: 13 additions & 0 deletions src/domain/session/session_manager_history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "turn_timing.hpp"
#include "session_history_page.hpp"
#include "utils/logger.hpp"
#include "utils/scope_exit.hpp"
#include <filesystem>

namespace acecode {
Expand All @@ -12,6 +13,18 @@ void SessionManager::on_message(const ChatMessage& msg) {
(void)try_on_message(msg);
}

bool SessionManager::try_on_message(const ChatMessage& msg) {
std::lock_guard<std::mutex> lk(mu_);
// Recording an error notice must not replace the storage failure that
// caused it, even when that notice also cannot be written to disk.
std::string original_error = msg.role == "error" && msg.metadata.is_object() &&
msg.metadata.value("transcript_only", false) ? last_error_ : std::string{};
ScopeExit preserve_error([this, original_error = std::move(original_error)]() mutable {
if (!original_error.empty()) last_error_.swap(original_error);
});
return try_on_message_locked(msg);
}

void SessionManager::record_turn_outcome(const std::string& outcome) {
if (!is_valid_turn_timing_status(outcome)) return;
std::lock_guard<std::mutex> lock(mu_);
Expand Down
Loading