self-run: the result events text, subtype and error flag are written to the unit's stdout, bounded to 4 KiB and marked when cut, so what claude concluded reaches the journal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 02:53:39 +01:00
commit f896fbaa75
3 changed files with 51 additions and 5 deletions

View file

@ -444,6 +444,33 @@ pub fn carry(mind: &dyn MindPort, clock: &dyn Clock, lender: &dyn Lender, spawne
Ok(outcome)
}
/// The most of a result's text that reaches the journal.
const RESULT_TEXT_LIMIT: usize = 4096;
/// What `claude` concluded: a result event's text, with its subtype and error
/// flag when it has them, written to the unit's stdout so it reaches the journal
/// before the transcript goes with the runtime directory. Bounded, marked when cut.
fn write_result(line: &str, out: &mut dyn Write) -> std::io::Result<()> {
let Ok(Value::Object(event)) = serde_json::from_str::<Value>(line) else { return Ok(()) };
if event.get("type").and_then(Value::as_str) != Some("result") {
return Ok(());
}
let mut head = String::from("result");
if let Some(subtype) = event.get("subtype").and_then(Value::as_str) {
head.push_str(&format!(" subtype={subtype}"));
}
if let Some(is_error) = event.get("is_error").and_then(Value::as_bool) {
head.push_str(&format!(" is_error={is_error}"));
}
let text = event.get("result").and_then(Value::as_str).unwrap_or("");
let mut end = text.len().min(RESULT_TEXT_LIMIT);
while !text.is_char_boundary(end) {
end -= 1;
}
let marker = if end < text.len() { "\n[truncated]" } else { "" };
writeln!(out, "{head}: {}{marker}", &text[..end])
}
/// The loop: facts in, one clock read per fact, actions out.
fn drive(mind: &dyn MindPort, clock: &dyn Clock, mut child: Box<dyn Child>, run: &PipelineRef, spent: &mut Spent, out: &mut dyn Write) -> Result<SelfRunOutcome> {
let user_line = |text: &str| json!({"type": "user", "message": {"role": "user", "content": text}}).to_string();
@ -456,6 +483,7 @@ fn drive(mind: &dyn MindPort, clock: &dyn Clock, mut child: Box<dyn Child>, run:
let fact = match child.next(wait) {
Read::Line(line) => {
spent.hear(&line);
write_result(&line, out)?;
match read_usage(&line) {
Usage::Request { id, prefix } => Event::Request { id, prefix },
Usage::Result => Event::Result,

View file

@ -622,6 +622,24 @@ mod seed_links {
}
}
#[test]
fn a_result_events_text_reaches_the_journal_with_its_subtype_and_error_flag() {
let line = json!({"type": "result", "subtype": "success", "is_error": false, "result": "all done"}).to_string();
let ran = go(vec![Out(line)]);
assert!(ran.out.contains("result subtype=success is_error=false: all done\n"), "{}", ran.out);
}
#[test]
fn a_long_result_is_cut_at_the_limit_on_a_character_boundary_and_marked() {
// One ASCII byte first, so the limit falls in the middle of a two-byte character.
let text = format!("a{}", "é".repeat(RESULT_TEXT_LIMIT));
let line = json!({"type": "result", "result": text}).to_string();
let ran = go(vec![Out(line)]);
let written = ran.out.lines().find(|l| l.starts_with("result")).unwrap();
assert_eq!(written, format!("result: a{}", "é".repeat(RESULT_TEXT_LIMIT / 2 - 1)));
assert!(ran.out.contains("\n[truncated]\n"), "{}", ran.out);
}
// ---- what is read -----------------------------------------------------------
#[test]
@ -805,7 +823,7 @@ fn a_jump_past_the_ceiling_plus_the_headroom_writes_the_flush_and_the_running_tu
// The headroom is measured from the prefix the flush was asked at (400k) and counts the requests after the
// first result only: 459_999 does not interrupt; 460_000 does, once, and m6 is never read.
assert_eq!(jump.count(&Event::Stopped(Stop::Interrupt)), 1, "{kinds:?}");
assert_eq!(jump.out, "request 1 prefix 290000\nrequest 2 prefix 400000\nrequest 3 prefix 470000\nrequest 4 prefix 459999\nrequest 5 prefix 460000\nsettled: billed 7 micro-USD\n");
assert_eq!(jump.out, "request 1 prefix 290000\nrequest 2 prefix 400000\nrequest 3 prefix 470000\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nrequest 4 prefix 459999\nrequest 5 prefix 460000\nsettled: billed 7 micro-USD\n");
assert_eq!(jump.ended().0, Cause::Signalled(Stop::Interrupt));
assert_eq!(flushes(&jump), 1);
}
@ -1014,7 +1032,7 @@ fn unreadable_and_foreign_lines_are_skipped_and_never_printed() {
Out(at("m1", 10)),
Out(result_line()),
]);
assert_eq!(ran.out, "request 1 prefix 10\nsettled: billed 7 micro-USD\n");
assert_eq!(ran.out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
let outcome = ran.outcome.as_ref().unwrap().to_string();
assert!(!ran.out.contains(canary) && !outcome.contains(canary));
assert!(!ran.written().iter().any(|text| text.contains(canary)), "nothing from the stream is written back");
@ -1214,7 +1232,7 @@ mod process {
let mut out = Vec::new();
let outcome = carry(&Seen::of(&mind), &SystemClock, &FakeLender::new(Answer::Grant), &Spawn(program), TEST_CAP_S, &mut out).unwrap();
assert_eq!(outcome, SelfRunOutcome::Ended { cause: Cause::FlushRequested, code: Some(0), requests: 2, prefix: 300_000 });
assert_eq!(String::from_utf8(out).unwrap(), "request 1 prefix 10\nrequest 2 prefix 300000\nsettled: billed 7 micro-USD\n");
assert_eq!(String::from_utf8(out).unwrap(), "request 1 prefix 10\nrequest 2 prefix 300000\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
}
#[test]

View file

@ -218,9 +218,9 @@ fn the_journal_says_what_was_billed_and_that_a_failed_settle_leaves_the_reservat
(outcome, String::from_utf8(out).unwrap())
};
let (outcome, out) = run(Answer::Grant);
assert_eq!(out, "request 1 prefix 10\nsettled: billed 7 micro-USD\n");
assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
let (failed, out) = run(Answer::GrantSettleFails);
assert_eq!(out, "request 1 prefix 10\nsettle failed: the connector bills the reservation\n", "the connector's words are not echoed");
assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettle failed: the connector bills the reservation\n", "the connector's words are not echoed");
assert_eq!(failed, outcome, "the run's own outcome stands");
}