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
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ loosely while pre-1.0 (breaking changes can land on minor bumps).
## [Unreleased]

### Fixed
- **Remote TUI: recoverable SSE `error` is not a failed turn.** `RemoteSseTurnConsumer::finish` copied accumulated `error` event text even when the terminal `done` event had `ok: true` (e.g. catalog skip of a stored agent whose JSON failed validation). `done` is authoritative: success clears the result error; failure still prefers `done.error` and falls back to prior `error` events when that field is empty.
- **Unreadable TUI sessions are not empty.** `session_json_is_empty` no
longer treats a JSON parse error (or a non-object blob) as an empty
transcript. Untitled 24h GC and `create_or_reuse` were hard-deleting
or overwriting sessions that could not be parsed.
- **Advise-gate cancel is not a bad bearer.** `POST /v1/advise/gate`
- **MCP string JSON-RPC ids.** `parse_response` now accepts a decimal-string
- **Reconcile rollback no longer wipes the workspace on a failed restore.**
Expand Down
7 changes: 6 additions & 1 deletion src/remote/sse_turn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ void RemoteSseTurnConsumer::handle_done(const JsonValue& payload) {
if (!ok_) {
const std::string err = payload.get_string("error");
if (!err.empty()) error_ = err;
} else {
// SSE `error` events are recoverable (catalog skip, transient
// upstream note). `done` is the terminal aggregate — a successful
// turn must not keep those strings as the result error.
error_.clear();
}
input_tokens_ = static_cast<int>(payload.get_number("input_tokens", 0));
output_tokens_ = static_cast<int>(payload.get_number("output_tokens", 0));
Expand All @@ -251,7 +256,7 @@ RemoteTurnResult RemoteSseTurnConsumer::finish(bool transport_cancelled) {
if (done_seen_) {
r.ok = ok_;
r.content = content_;
r.error = error_;
r.error = ok_ ? std::string{} : error_;
return r;
}
// Stream closed without done — treat as failure.
Expand Down
39 changes: 39 additions & 0 deletions tests/test_remote_connect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,45 @@ TEST_CASE("RemoteSseTurnConsumer surfaces done.ok=false") {
CHECK(result.error == "upstream failed");
}

TEST_CASE("RemoteSseTurnConsumer drops recoverable error on done.ok=true") {
OutputQueue queue;
StreamRenderer renderer(kMasterStream, queue);
RemoteSseTurnConsumer consumer(renderer, queue);
consumer.on_event("error",
R"({"message":"skipping stored agent 'broken' for tenant 1: invalid json"})");
consumer.on_event("text", R"({"delta":"ok"})");
consumer.on_event("done",
R"({"ok":true,"content":"ok","input_tokens":3,"output_tokens":1})");
auto result = consumer.finish(false);
CHECK(result.ok);
CHECK(result.content == "ok");
CHECK(result.error.empty());
CHECK(result.input_tokens == 3);
CHECK(result.output_tokens == 1);
}

TEST_CASE("RemoteSseTurnConsumer keeps SSE error when done.ok=false has no error") {
OutputQueue queue;
StreamRenderer renderer(kMasterStream, queue);
RemoteSseTurnConsumer consumer(renderer, queue);
consumer.on_event("error", R"({"message":"history load failed"})");
consumer.on_event("done", R"({"ok":false,"content":""})");
auto result = consumer.finish(false);
CHECK_FALSE(result.ok);
CHECK(result.error == "history load failed");
}

TEST_CASE("RemoteSseTurnConsumer prefers done.error over earlier SSE error") {
OutputQueue queue;
StreamRenderer renderer(kMasterStream, queue);
RemoteSseTurnConsumer consumer(renderer, queue);
consumer.on_event("error", R"({"message":"skipping stored agent 'x'"})");
consumer.on_event("done", R"({"ok":false,"error":"agent not found","content":""})");
auto result = consumer.finish(false);
CHECK_FALSE(result.ok);
CHECK(result.error == "agent not found");
}

TEST_CASE("RemoteSseTurnConsumer cancel without done") {
OutputQueue queue;
StreamRenderer renderer(kMasterStream, queue);
Expand Down
Loading