Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48d6319f7 | ||
|
|
9aaf8b93fb |
1 changed files with 33 additions and 14 deletions
|
|
@ -75,18 +75,27 @@ impl Sentinels {
|
||||||
if matches!(ty, Some("assistant" | "user")) {
|
if matches!(ty, Some("assistant" | "user")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The terminal `result` event carries the model's final answer in its
|
// A `result` event's `result` field is model-authored answer text on a
|
||||||
// `result` field — model-authored text, same trust level as an
|
// SUCCESSFUL turn — same trust level as an assistant message. A marker
|
||||||
// assistant message. A marker quoted there (e.g. the model explaining
|
// quoted there (the model explaining *this* code, or echoing an error
|
||||||
// *this* code, or echoing an error string back) would falsely trip a
|
// string back) would falsely trip a sentinel and needlessly kill /
|
||||||
// sentinel and needlessly kill/downgrade the turn. So for a `result`
|
// downgrade the turn, so for a successful result we scrub `result`
|
||||||
// event, scan the control fields (`subtype` / `error` / `is_error`)
|
// before scanning and rely on the other (claude-authored) fields.
|
||||||
// but scrub the model-authored `result` field first; a genuine
|
//
|
||||||
// prompt-too-long/auth/session signal lives in those control fields
|
// On a FAILED result the same `result` field instead carries
|
||||||
// (it's emitted *instead of* a successful model answer), never in the
|
// claude-code's OWN failure text, emitted *instead of* a model answer.
|
||||||
// `result` text. Other control events (`error` / `system`) are
|
// The real event (verified against captured stream-json) is:
|
||||||
// claude-authored end to end, so they scan raw.
|
// {"type":"result","is_error":true,"subtype":"success",
|
||||||
if ty == Some("result") {
|
// "result":"Prompt is too long","terminal_reason":"blocking_limit"}
|
||||||
|
// Note `subtype` is "success" even on a hard failure — so `is_error` is
|
||||||
|
// the only reliable discriminator, and the model can't forge it (the CLI
|
||||||
|
// sets it from the real outcome). Scan those raw. Scrubbing `result`
|
||||||
|
// unconditionally (the original fix) blinded prompt-too-long / auth
|
||||||
|
// detection. Other control events are claude-authored throughout, so
|
||||||
|
// they also scan raw.
|
||||||
|
let error_result = ty == Some("result")
|
||||||
|
&& event.get("is_error").and_then(serde_json::Value::as_bool) == Some(true);
|
||||||
|
if ty == Some("result") && !error_result {
|
||||||
let mut scrubbed = event.clone();
|
let mut scrubbed = event.clone();
|
||||||
if let Some(obj) = scrubbed.as_object_mut() {
|
if let Some(obj) = scrubbed.as_object_mut() {
|
||||||
obj.remove("result");
|
obj.remove("result");
|
||||||
|
|
@ -157,11 +166,21 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn control_event_marker_is_detected() {
|
fn genuine_failure_result_is_detected() {
|
||||||
|
// Both fixtures are the real claude-code failure shape, verified against
|
||||||
|
// captured stream-json: the marker lives in the `result` field with
|
||||||
|
// `is_error: true` and — counterintuitively — `subtype: "success"`. The
|
||||||
|
// earlier unconditional `result` scrub blinded this; `is_error` is the
|
||||||
|
// discriminator that restores it (and the model can't forge it).
|
||||||
let s = Sentinels::default();
|
let s = Sentinels::default();
|
||||||
let raw = r#"{"type":"result","subtype":"error","error":"Prompt is too long"}"#;
|
let raw = r#"{"type":"result","is_error":true,"subtype":"success","result":"Prompt is too long","terminal_reason":"blocking_limit","stop_reason":"stop_sequence"}"#;
|
||||||
s.scan_stdout_json(&json(raw), raw);
|
s.scan_stdout_json(&json(raw), raw);
|
||||||
assert!(matches!(s.soft_error(), Some(Error::PromptTooLong)));
|
assert!(matches!(s.soft_error(), Some(Error::PromptTooLong)));
|
||||||
|
|
||||||
|
let s = Sentinels::default();
|
||||||
|
let raw = r#"{"type":"result","is_error":true,"subtype":"success","result":"Failed to authenticate. API Error: 401 Invalid authentication credentials","api_error_status":401}"#;
|
||||||
|
s.scan_stdout_json(&json(raw), raw);
|
||||||
|
assert!(matches!(s.soft_error(), Some(Error::AuthFailed)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue