From fb02d4145f9f803288e204419721e5640dd5fc77 Mon Sep 17 00:00:00 2001 From: BitmapAsset Date: Mon, 26 Oct 2026 20:49:48 -0700 Subject: [PATCH] Monitor: replay from the header when the persona has an expectation block and the strand has checkpoints The expectation block (RFC 0009) folds every event line into its memory, and no checkpoint line carries that memory, so `Replay::open_at` refuses to start there. `monitor --once` started at the last checkpoint and exited 1 with that refusal on a valid strand; `--follow`, `--serve` and `--dir` fell back to the header only because the error was dropped. `Watch::start` now decides where a replay starts: from the header when the persona has an expectation block, else from the last checkpoint, as before. Both start paths use it; appended lines still replay one by one from the state in memory. `live verify` and `verify --from-checkpoint` are unchanged. A fixture (the tutor with an expectation block, 22 events, 4 checkpoints, recorded on the fixed clock) opens in `--once` (event 22), streams in `--serve` and lists with its n in `--dir`; the same events through the tutor without the block still start at the last checkpoint. A 5,000-event life with the block opens in about 7.5 s (about 650 events a second on a 16-vCPU Linux VM). --- CHANGELOG.md | 9 + docs/persona.md | 6 +- probbit-cli/src/monitor.rs | 49 ++++- .../monitor/tutor-expect.events.jsonl | 22 +++ .../fixtures/monitor/tutor-expect.strand | 27 +++ .../tests/fixtures/monitor/tutor-expect.yaml | 180 ++++++++++++++++++ probbit-cli/tests/monitor.rs | 36 ++++ 7 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl create mode 100644 probbit-cli/tests/fixtures/monitor/tutor-expect.strand create mode 100644 probbit-cli/tests/fixtures/monitor/tutor-expect.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e4d94..5530304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented here. The format follows ## Unreleased +### Fixed + +- `probbit monitor` opens a strand with checkpoint lines when its persona has an `expectation` block: it replays such a + strand from the header (`--once`, `--follow`, `--serve`, and each strand of `--dir` by its own persona) instead of refusing + it at the last checkpoint, which does not carry the expectation's memory. Other personas still start at the last + checkpoint; `live verify` is unchanged. + +### Documentation + - Documentation, from a dress rehearsal of the published v0.13.0 artifacts (the release page's Mac app zip opened as a download, the README and REPRODUCE.md commands with the released binary on Linux and macOS, `probbit-stance` 0.2.0 from npm): `docs/monitor-app.md` says to remove the quarantine mark before the first open and how to recover after a refused diff --git a/docs/persona.md b/docs/persona.md index 2d8e2f6..a49bfd4 100644 --- a/docs/persona.md +++ b/docs/persona.md @@ -1573,7 +1573,11 @@ check that inference. Hashes alone do not authenticate a writer or detect a whol anchor. Historical measurement before this hardening, on an Apple M4: a 10,000-event strand of a drives persona (4 goals, a floor, learning; 3,126,444 bytes, 10 checkpoint lines) opens in `monitor --once` from its last checkpoint in 0.005 s (load 3.3), where a full `verify` takes 192 s. A replay from a checkpoint costs what the events after it cost: 500 events past the last one took 26 s to draw -(load 6-13, about 50 ms an event for this persona), so K bounds the wait; a smaller K costs a few KB per checkpoint line. A strand without checkpoint or control lines (shorter than K events, or `--checkpoint-every 0`) is the +(load 6-13, about 50 ms an event for this persona), so K bounds the wait; a smaller K costs a few KB per checkpoint line. A persona with an `expectation` block (RFC 0009) +is the exception: its expectation memory is a fold of every event line, and no checkpoint line carries it, so `monitor` +(`--once`, `--follow`, `--serve`, each strand of `--dir` by its own persona) replays such a strand from its header, every +line, and `verify --from-checkpoint` still refuses it. The cost is a full replay when the strand is opened: on a 16-vCPU Linux VM, a 5,000-event life of the tutor with the block (10 checkpoint lines) opens in `monitor --once` in 7.4 to 7.8 s, about 650 events a second, where the same life without the block opens from its last checkpoint in under 0.05 s; after +that, appended lines replay one by one from the state in memory, as for any strand. A strand without checkpoint or control lines (shorter than K events, or `--checkpoint-every 0`) is the same format as in 0.8.0. New strand headers record the current engine version, so their chain hashes differ; existing 0.8.0 strands remain replayable. Binaries before 0.8.0 do not read checkpoint or control lines. diff --git a/probbit-cli/src/monitor.rs b/probbit-cli/src/monitor.rs index b0ac76d..614dfe2 100644 --- a/probbit-cli/src/monitor.rs +++ b/probbit-cli/src/monitor.rs @@ -127,6 +127,13 @@ impl Watch { let rep = Replay::open(head)?; let meta = Meta::of(&rep); let n = meta.moods.len(); Ok(Watch { rep, meta, last: None, spark: vec![VecDeque::new(); n], bad: None }) } + /// Where the replay of `lines` (the header first) starts: the last checkpoint line (index), or None for the header. RFC 0009: + /// a persona with an expectation block folds every event line into its memory, which no checkpoint carries, so its strand + /// replays from the header (every line, slower; `open_at` refuses that shortcut); any other persona starts at the last one + fn start(lines: &[&str]) -> Option { + let k = crate::live::last_checkpoint(lines, 0).filter(|k| *k >= 2)?; + (!Replay::open(lines[0]).is_ok_and(|r| r.live.p.expect.is_some())).then_some(k) + } /// The replay from a checkpoint line (`live::Replay::open_at`; `before` = the event line before it): the lines before it are /// not replayed, and the board starts at the checkpoint's event (its stance, the inputs `before` logs, its learned deltas) fn open_at(head: &str, before: &str, cp: &str, at: usize) -> Result { @@ -644,10 +651,11 @@ impl Followed { fn poll(&mut self, eng: Engine, each: &mut dyn FnMut(&Watch)) -> Option { match self.tail.poll() { Got::Lines(ls) => { - // the first read of a strand with checkpoint lines starts at the last one: its event is drawn at once + // the first read of a strand with checkpoint lines starts at the last one (`Watch::start`): its event is drawn at + // once; after that, appended lines replay from the state in memory let mut skip = 0; if self.w.is_none() && self.bad_head.is_none() { let refs: Vec<&str> = ls.iter().map(String::as_str).collect(); - if let Some(k) = crate::live::last_checkpoint(&refs, 0).filter(|k| *k >= 2) { + if let Some(k) = Watch::start(&refs) { if let Ok(x) = Watch::open_at(refs[0], refs[k - 1], refs[k], k + 1) { each(&x); self.w = Some(x); skip = k + 1; } } } for l in ls.into_iter().skip(skip) { if let Some(x) = &mut self.w { let n = x.rep.live.n; x.feed(&l, eng); if x.rep.live.n != n && x.bad.is_none() { each(x); } continue; } if self.bad_head.is_none() { match Watch::open(&l) { Ok(x) => self.w = Some(x), Err(e) => self.bad_head = Some(e) } } } @@ -746,8 +754,9 @@ pub fn cmd(args: &[String]) { if head.is_empty() { crate::fail(&format!("monitor: {path} is empty: not a strand")) } if !strand(head) { crate::fail(&format!("monitor: {path} is not a probbit strand (format 1)")) } let note = (!rest.is_empty() && !lines.is_empty()).then(|| format!("line {} is incomplete (no newline at its end): not replayed", lines.len() + 1)); - // a strand with checkpoint lines replays from the last one (the board starts at its event and draws the latest one) - let start = crate::live::last_checkpoint(&lines, 0); + // a strand with checkpoint lines replays from the last one (the board starts at its event and draws the latest one), unless + // its persona has an expectation block: then from the header (`Watch::start`) + let start = Watch::start(&lines); let opened = match start { Some(k) => Watch::open_at(head, lines[k - 1], lines[k], k + 1), None => Watch::open(head) }; let (w, bad_head) = match opened { Ok(mut w) => { for l in lines.iter().skip(start.map_or(1, |k| k + 1)) { w.feed(l, eng); } (Some(w), None) } Err(e) => (None, Some(e)) }; let fr = frame(w.as_ref(), bad_head.as_ref(), &path, note.as_deref(), &s); @@ -1158,6 +1167,38 @@ mod tests { let _ = std::fs::remove_file(&f); } + /// RFC 0009: a persona with an expectation block replays a strand with checkpoint lines from its header (the checkpoint does + /// not carry the expectation's memory): `--once` and the first poll of `--follow` / `--serve` draw the latest event with no + /// checkpoint start, and lines appended after it replay from the state in memory, one by one + #[test] + fn an_expectation_persona_replays_from_the_header() { + let (p, doc) = persona::load(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/monitor/tutor-expect.yaml")).unwrap(); + assert!(p.expect.is_some(), "the fixture has an expectation block"); + let (mut lv, header) = Live::start(p.clone(), &doc, persona::init(&p, Some(2), true, &run), Clock::Fixed, "probbit test"); + let mut text = format!("{header}\n"); + let add = |lv: &mut Live, text: &mut String, t: u32| { let ev = json::parse(&format!(r#"{{"praise":{},"elapsed_hours":1}}"#, t % 3 == 0)).unwrap(); + let (s, line) = lv.event(&ev, &run).unwrap(); *text += &line; text.push('\n'); if lv.n % 5 == 0 { *text += &lv.checkpoint(&s); text.push('\n'); } }; + for t in 0..17u32 { add(&mut lv, &mut text, t); } + let lines = live::lines(&text); let k = live::last_checkpoint(&lines, 0).unwrap(); + assert_eq!((lines.iter().filter(|l| l.starts_with(r#"{"checkpoint":"#)).count(), Watch::start(&lines)), (3, None), "3 checkpoints, no checkpoint start"); + assert!(Replay::open_at(lines[0], lines[k - 1], lines[k], k + 1).is_err_and(|e| e.1.contains("expectation block")), "the shortcut itself still refuses it"); + let f = std::env::temp_dir().join(format!("probbit-monitor-expect-{}.strand", std::process::id())); std::fs::write(&f, &text).unwrap(); + let mut fl = Followed::new(f.to_str().unwrap()); let mut seen = vec![]; + assert_eq!(fl.poll(&run, &mut |w: &Watch| seen.push(w.rep.live.n)), Some(false)); + let w = fl.w.as_ref().unwrap(); assert!(w.bad.is_none(), "{:?}", w.bad); + assert_eq!((w.rep.from, w.rep.live.n, seen.first().copied(), seen.len()), (None, 17, Some(1), 17), "every event replayed from the header"); + let mut full = Watch::open(lines[0]).unwrap(); for l in &lines[1..] { full.feed(l, &run); } + assert_eq!(persona::canon(&w.last.as_ref().unwrap().doc), persona::canon(&full.last.as_ref().unwrap().doc)); + assert_eq!(frame_json(fl.w.as_ref(), None, None).get("from_checkpoint"), Some(&Json::Null)); + // appended: only the new lines replay, on the state in memory + for t in 17..21u32 { add(&mut lv, &mut text, t); } std::fs::write(&f, &text).unwrap(); + seen.clear(); assert_eq!(fl.poll(&run, &mut |w: &Watch| seen.push(w.rep.live.n)), Some(false)); + assert_eq!(seen, vec![18, 19, 20, 21]); assert!(fl.w.as_ref().unwrap().bad.is_none()); + assert_eq!(fl.w.as_ref().unwrap().rep.summary(), live::verify(&text, &run).unwrap(), "ends where verify does"); + let _ = std::fs::remove_file(&f); + // the tutor without the block takes the shortcut: `following_starts_at_the_last_checkpoint` + } + /// Watch a strand line by line: every document's sha256 is its line's `stance` digest, and the replay ends where `verify` does fn replays_to_its_digests(text: &str, name: &str) -> Watch { let lines = live::lines(text); diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl b/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl new file mode 100644 index 0000000..5d77426 --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl @@ -0,0 +1,22 @@ +{"praise":true,"elapsed_hours":0.5} +{"loss":true,"elapsed_hours":1} +{"elapsed_hours":2} +{"error":true,"elapsed_hours":13} +{"praise":true,"elapsed_hours":1} +{"loss":true,"elapsed_hours":0.5} +{"elapsed_hours":1} +{"error":true,"elapsed_hours":2} +{"praise":true,"elapsed_hours":13} +{"loss":true,"elapsed_hours":1} +{"elapsed_hours":0.5} +{"error":true,"elapsed_hours":1} +{"praise":true,"elapsed_hours":2} +{"loss":true,"elapsed_hours":13} +{"elapsed_hours":1} +{"error":true,"elapsed_hours":0.5} +{"praise":true,"elapsed_hours":1} +{"loss":true,"elapsed_hours":2} +{"elapsed_hours":13} +{"error":true,"elapsed_hours":1} +{"praise":true,"elapsed_hours":0.5} +{"loss":true,"elapsed_hours":1} diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.strand b/probbit-cli/tests/fixtures/monitor/tutor-expect.strand new file mode 100644 index 0000000..57fd98a --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.strand @@ -0,0 +1,27 @@ +{"probbit_strand":1,"engine":"probbit 0.13.0","persona":{"name":"Pip","version":"1.1.0","digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c"},"seed":2,"state":{"digest":"sha256:eef51fdf809d2a63d5f9f2fe90bca59c5f319881cdebe0ce6fb35de60b7aaf92","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[],"error":[]},"mood":{"arousal":[0,0,0],"valence":[0,0,0]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"gentle","emoji":"sparse","formality":"casual","humour":"light","initiative":"suggest","valence":"even","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":0},"document":{"probbit_persona":1,"identity":{"name":"Pip","version":"1.1.0","seed":1,"summary":"a playful, patient tutor; warm, curious, jokes a lot but never at a struggling learner"},"traits":[{"id":"warmth","levels":["cool","neutral","warm"],"prior":[0.05,0.3,0.65],"spread":0.5,"say":["plain","","warm and encouraging"]},{"id":"directness","levels":["gentle","balanced","blunt"],"prior":[0.45,0.45,0.1],"spread":0.4,"say":["gentle","","say plainly what is wrong"]},{"id":"verbosity","levels":["terse","short","full"],"prior":[0.1,0.4,0.5],"spread":0.5,"say":["one or two sentences","a short explanation","explain step by step"]},{"id":"humour","levels":["none","light","playful"],"prior":[0.15,0.45,0.4],"spread":0.6,"say":["no jokes","a light joke is welcome","playful"]},{"id":"formality","levels":["casual","neutral","formal"],"prior":[0.6,0.35,0.05],"spread":0.3,"say":["casual","","formal"]},{"id":"initiative","levels":["follow","suggest","lead"],"prior":[0.2,0.5,0.3],"spread":0.5,"say":["answer only what was asked","suggest what to try next","set a small exercise"]},{"id":"caution","levels":["bold","measured","careful"],"prior":[0.25,0.55,0.2],"spread":0.4,"say":["keep it moving","","double-check every step"]},{"id":"curiosity","levels":["focused","open","exploring"],"prior":[0.15,0.4,0.45],"spread":0.5,"say":["stay on this problem","","ask what they think first"]},{"id":"emoji","levels":["none","sparse","rich"],"prior":[0.2,0.5,0.3],"spread":0.5,"say":["no emoji","one emoji at most","emoji welcome"]}],"moods":[{"id":"valence","levels":["down","even","up"],"prior":[0.1,0.5,0.4],"spread":0.4,"inertia":0.5,"half_life_hours":12},{"id":"arousal","levels":["calm","steady","keyed"],"prior":[0.3,0.4,0.3],"spread":0.3,"inertia":0.4}],"couplings":[{"vars":["valence","humour"],"align":1},{"vars":["valence","emoji"],"align":0.6},{"vars":["arousal","verbosity"],"align":-0.4},{"vars":["warmth","formality"],"align":-0.6},{"vars":["humour","formality"],"align":-0.6},{"vars":["curiosity","verbosity"],"align":0.3},{"vars":["humour","emoji"],"align":0.4}],"inputs":[{"id":"sentiment","kind":"level","levels":["negative","neutral","positive"],"default":"neutral","say":{"negative":"learner upset","positive":"learner upbeat"},"effects":{"negative":{"valence":-1,"warmth":0.6,"humour":-1,"directness":-0.6},"positive":{"valence":0.8,"humour":0.5,"emoji":0.3}}},{"id":"confused","kind":"flag","say":"learner confused","effects":{"verbosity":0.9,"humour":-0.6,"warmth":0.4,"curiosity":0.6,"initiative":0.3,"directness":-0.3}},{"id":"error","kind":"flag","say":"my last answer was wrong","effects":{"caution":1.2,"verbosity":-0.3,"valence":-0.4,"humour":-0.4}},{"id":"loss","kind":"flag","say":"learner reports a failure","effects":{"valence":-1.6,"warmth":1,"humour":-2,"emoji":-0.8}},{"id":"praise","kind":"flag","say":"praise","reactivity_spread":0.4,"effects":{"valence":1.2,"humour":0.5,"emoji":0.5}},{"id":"criticism","kind":"flag","say":"criticism","effects":{"valence":-0.6,"caution":0.6,"humour":-0.4}},{"id":"stakes","kind":"number","say":"stakes","effects":{"caution":1.4,"humour":-0.8,"emoji":-0.6}},{"id":"time_pressure","kind":"number","say":"time pressure","effects":{"verbosity":-1.8,"curiosity":-1.2,"humour":-0.4,"arousal":0.6}},{"id":"claim_done","kind":"flag","say":"about to say it is solved","effects":{"caution":0.5,"step.check":0.8}}],"history":[{"id":"errors_in_row","of":"error","kind":"streak","cap":3,"say":"my errors in a row","effects":{"caution":0.5,"humour":-0.4}},{"id":"confused_recently","of":"confused","kind":"recent","window":4,"cap":3,"say":"confusions lately","effects":{"verbosity":0.3,"warmth":0.2}}],"habits":[{"id":"no_jokes_on_loss","when":{"loss":true},"then":{"humour":["none"],"emoji":{"at_most":"sparse"}},"say":"no jokes, be kind"},{"id":"no_play_when_upset","when":{"sentiment":"negative"},"then":{"humour":{"at_most":"light"}},"say":"go easy on the jokes"},{"id":"confused_no_playful","when":{"confused":true},"then":{"humour":{"at_most":"light"},"verbosity":{"at_least":"short"}},"say":"slow down, no clowning","priority":10},{"id":"after_error_check","when":{"error":true},"then":{"caution":{"at_least":"prev+1"},"verbosity":{"at_most":"prev-1"}},"say":"shorter, double-check"},{"id":"flourish_budget","rules":{"linear":[{"terms":[["humour","playful",2],["humour","light",1],["emoji","rich",2],["emoji","sparse",1]],"limit":3}]},"say":"one flourish at a time"},{"id":"no_formal_emoji","rules":{"tables":[{"vars":["formality","emoji"],"forbid":[["formal","rich"],["formal","sparse"]]}]},"say":"no emoji when formal"},{"id":"check_last","rules":{"precedes":[{"before":"step.explain","after":"step.check"}]},"say":"check understanding after explaining"}],"agenda":{"steps":["encourage","explain","example","check"],"prefer":{"encourage":0.5,"explain":0.3,"example":-0.1,"check":-0.6}},"engine":{"on_conflict":"yield"},"line":{"max_tokens":40,"order":["verbosity","humour","warmth","initiative","curiosity","emoji","directness","caution","formality"]},"expectation":{}}} +{"deviation":{"input":0,"pred":0.20781,"self":0.20781},"inputs":{"elapsed_hours":0.5,"praise":true},"n":1,"prev":"sha256:7e9df458eb9b480cbbe1cc35394eae47df68d2beaf719d00a243db88f6285827","stance":"sha256:016fef69c5e036b83b4b5e8a704a166053bf75333fcfe643b91a01798431c113","state":"sha256:1bcf9a02bf0454969e10442b3d4ebefc18f69df15734c3e5af5e0d0189f0ef60"} +{"deviation":{"input":0.69315,"pred":2.8423,"self":2.2116},"inputs":{"elapsed_hours":1,"loss":true},"n":2,"prev":"sha256:ae314cf23a00bbd7e4a0a78ba77631649bd00dd4b37c1f55225d1e090f4f3010","stance":"sha256:afb1164f2022b3646092919c3627a6652132f9d6b86cbc747f6394eab02de22e","state":"sha256:c79a11b1406d5373bcb2f873fc3d99857b6e78a23a75be3b4de9a43f830a2114"} +{"deviation":{"input":1.0914,"pred":0.091607,"self":0},"inputs":{"elapsed_hours":2},"n":3,"prev":"sha256:74b1f36de9ffd3fb0fe5a8e08eb36941216d97dd08579f18f7af003b0c935ea4","stance":"sha256:541049d8ecde4c92f08cb234ea6cbf5040017d93dc276b16f457120b24c967d3","state":"sha256:207b7f6a8bb56cefac90f79dbb8cea06f5db518abd4229a937ed8a684ccf85f2"} +{"deviation":{"input":0.67706,"pred":2.7765,"self":2.8336},"inputs":{"elapsed_hours":13,"error":true},"n":4,"prev":"sha256:93b2cd22ff409fba27ba33abd44bb3214295e611fe0e8165271b9c3168d8b64e","stance":"sha256:019959c6bf2d5d60eadb2e4c89fefe7151dd67c6491b1f2eb4c161b7448f0a86","state":"sha256:e85069f361ff10bac7fd3f35cfb5e2ecd148f72214c2bba5ff3cfbd4992ea9fa"} +{"deviation":{"input":1.6487,"pred":0.66312,"self":0.21638},"inputs":{"elapsed_hours":1,"praise":true},"n":5,"prev":"sha256:2e12dab1a54c0b75bd06a29ff191cef7ae34b9938dbb20cbf5b3cbfd2e7edcf2","stance":"sha256:8daaddad69d74b08f223aba03aafa4ec0a5f90decb3bf9ec6d3d9486e1b4fb06","state":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198"} +{"checkpoint":5,"prev":"sha256:ebb3d4d0ca05407c4ab2a494edc5ab495a0227b24a303fd525e0a5520b281729","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.6487454934364103,"pred":0.663115886379269,"registers":false,"self":0.2163809366922902,"z":1.354697},"engine":{"program":"sha256:95a9314d809c73dc9d9b7a9496fa816337d890c576fe0ceed2d017ff4550cd44","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["flourish_budget","no_formal_emoji","check_last"],"bound":["flourish_budget"],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":false,"praise":true,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: one flourish at a time; explain step by step; a light joke is welcome; warm and encouraging; suggest what to try next; ask what they think first.","line_tokens":39,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"up","odds":{"down":0.085635,"even":0.417117,"up":0.497249},"p":0.497249,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.037498,"rich":0.640842,"sparse":0.32166},"p":0.640842,"released":true},"formality":{"level":"casual","odds":{"casual":0.755025,"formal":0.001089,"neutral":0.243887},"p":0.755025,"released":true},"humour":{"level":"light","odds":{"light":0.554496,"none":0.386132,"playful":0.059371},"p":0.554496,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.01642,"neutral":0.183144,"warm":0.800436},"p":0.800436,"released":true}},"state_digest":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198","status":"ok","turn":4,"unsure":[],"why":"one flourish at a time (habit); praise -> valence up, humour up"},"state":{"digest":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,false,false,true,false]},"mood":{"arousal":[0,0,0],"valence":[-0.348016,0,0.348016]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":5}} +{"deviation":{"input":1.8212,"pred":1.8311,"self":2.1799},"inputs":{"elapsed_hours":0.5,"loss":true},"n":6,"prev":"sha256:691b6399093b47507c5ff3a2159d7ee44a75eabf9961d436161cdc7e2ad54f19","stance":"sha256:96bc79dd0fd3ced3867e8b3a37e83662fd983631938a22e727b6be78c5c0db94","state":"sha256:9cd5ea7e03aa5a16de710ede3ea7c3bf7fbb0dcaeb9d5b02c35fe143fc2a8e46"} +{"deviation":{"input":1.2391,"pred":0.1417,"self":0},"inputs":{"elapsed_hours":1},"n":7,"prev":"sha256:33b8db219356dab0921c72ad7ec386640708520a6131b814b122c99768ccba42","stance":"sha256:6c39be8da20202ed2744771641966f0e5ec87ea0cb708b7a85c64b4f4086b268","state":"sha256:0a32589256e6e254996652de8f423d03d5bda926b2b81eb0b77896e66f4913c8"} +{"deviation":{"input":2.0886,"pred":2.0414,"self":2.8317},"inputs":{"elapsed_hours":2,"error":true},"n":8,"prev":"sha256:efa74ba1c5e4ba18c5d1163866a6349ded96d042b81ac8ad19966da22bc75a40","stance":"sha256:7a662e50d3bad0352781a7d641e654b89b6f77d0489269c5838d4410021725d3","state":"sha256:2510474026de865306b0aed7b69f11e857eeff840006a4ff5f4c881cbcb4a6c7"} +{"deviation":{"input":1.5454,"pred":0.73484,"self":0.21337},"inputs":{"elapsed_hours":13,"praise":true},"n":9,"prev":"sha256:867188510f981fa92463e085740d9a0cf26f53f2344ca75a76baf26252cd691d","stance":"sha256:30f92c2616e3b38901fc660f61b4556fe18f8fe7cd60601fe729ef82b6294efb","state":"sha256:fbe42a6a03239aef066ddcd908f8b65dea2cf4d2349d6ba62f9ad192630acdb9"} +{"deviation":{"input":1.6406,"pred":1.7092,"self":2.1896},"inputs":{"elapsed_hours":1,"loss":true},"n":10,"prev":"sha256:8e0b586b21d0ffeecd6ed1a91aed3b53def6f5d87b8d7e4bd0d8c51bb5f23308","stance":"sha256:f764a6f800ac7364e6c76c672ce7b8dc001fb7d8f63f8cd33107a6d83e334dc5","state":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd"} +{"checkpoint":10,"prev":"sha256:cc852f6ea39d2367b6123a6066a8ada50914135bab5008ee319e270a158b0a61","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.6405653534874491,"pred":1.709166276250666,"registers":true,"self":2.1895837322588894,"z":2.288624},"engine":{"program":"sha256:b82832d8dc76cef7147d0b343220cf0b8e413148667b2d51c966c7e45b4c9d0f","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["no_jokes_on_loss","flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":true,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: no jokes, be kind; one emoji at most; explain step by step; warm and encouraging; suggest what to try next; ask what they think first; casual.","line_tokens":38,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"down","odds":{"down":0.541495,"even":0.405777,"up":0.052728},"p":0.541495,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"sparse","odds":{"none":0.416584,"rich":0,"sparse":0.583416},"p":0.583416,"released":true},"formality":{"level":"casual","odds":{"casual":0.690526,"formal":0.013291,"neutral":0.296183},"p":0.690526,"released":true},"humour":{"level":"none","odds":{"light":0,"none":1,"playful":0},"p":1,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.002982,"neutral":0.08223,"warm":0.914788},"p":0.914788,"released":true}},"state_digest":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd","status":"ok","turn":9,"unsure":[],"why":"learner reports a failure -> humour down, valence down"},"state":{"digest":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,false,true,false,false]},"mood":{"arousal":[0,0,0],"valence":[0.614733,0,-0.614733]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"sparse","formality":"casual","humour":"none","initiative":"suggest","valence":"down","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":10}} +{"deviation":{"input":1.2831,"pred":0.1713,"self":0},"inputs":{"elapsed_hours":0.5},"n":11,"prev":"sha256:0399dee7985c4b6e32fe79505187d1edde3519dd9c1bb8bd3c432cdb36435951","stance":"sha256:61deb67957b520ae6c81fb86e6baed41a10d13c37ec579f97b5515119e05180a","state":"sha256:9b0fffb260a9d2d317b8c6d2a2704a6c38331923aaad66822ae32f81019495dd"} +{"deviation":{"input":1.8024,"pred":1.8623,"self":2.8315},"inputs":{"elapsed_hours":1,"error":true},"n":12,"prev":"sha256:4b98762c333e63d5d0a81398aeb4236fb0ce6cebeabd737ee9675b1246adc7fa","stance":"sha256:d5276ba7bbdf776c324f04cfb5e00d06f98081ef0a03ef09aad2de2dce78c2d7","state":"sha256:daffac198a0ce7f3a8cdf7270c1807ce41c54f6918e37bcadde7755a4ea1866b"} +{"deviation":{"input":1.5086,"pred":0.76222,"self":0.21827},"inputs":{"elapsed_hours":2,"praise":true},"n":13,"prev":"sha256:390563c827d2aad8bd94bb20f77413258dee0175ddb37c5fd69ac6c86c0d0709","stance":"sha256:13ff4842a26ea33d98dfe343fd835a3f14c93011f43b142a2a86abc83076309e","state":"sha256:f0ccc4aed92c3f03b6dfc356d4231660bc643d164cd1d8e402bcb6444b474eeb"} +{"deviation":{"input":1.5725,"pred":1.628,"self":2.1228},"inputs":{"elapsed_hours":13,"loss":true},"n":14,"prev":"sha256:359f6e4df83d6fba50bb60abc1e97fa79f51134784456d48b0ebbdea622a4f23","stance":"sha256:f586b7c7a2643eb010d82d99ee5314e9abd215716ea9dd36b005bf4d18ae957f","state":"sha256:2c2d7b9e7e66d7dd7050ecf7101666ecbfe9be0ebe0abbf31c0d1be19c64fc32"} +{"deviation":{"input":1.3042,"pred":0.18472,"self":0},"inputs":{"elapsed_hours":1},"n":15,"prev":"sha256:af88a285bfb51bdaa1d5af3c3a0e080e600c3b98ff93d7b79e09085cb99b9316","stance":"sha256:0a186aad66b1f2b853088abda07fbf6395730994c69599082b30a8d57d29f7b6","state":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba"} +{"checkpoint":15,"prev":"sha256:62d72cf3d2a58dbdf915c6f3a5577a461242f38707c4efdc1d0bd2047c12c320","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.3042172408804977,"pred":0.18472371562460088,"registers":false,"self":0,"z":-0.044533},"engine":{"program":"sha256:9ad02ae3f33a68b43dcf5443385d4ea81abbcd26ea557efd1f6314a9987434f0","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":false,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: no jokes; explain step by step; warm and encouraging; suggest what to try next; ask what they think first; emoji welcome; casual.","line_tokens":35,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"even","odds":{"down":0.25337,"even":0.4997,"up":0.24693},"p":0.4997,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.101772,"rich":0.480668,"sparse":0.41756},"p":0.480668,"released":true},"formality":{"level":"casual","odds":{"casual":0.72774,"formal":0.003406,"neutral":0.268853},"p":0.72774,"released":true},"humour":{"level":"none","odds":{"light":0.377719,"none":0.590719,"playful":0.031562},"p":0.590719,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.01711,"neutral":0.185902,"warm":0.796988},"p":0.796988,"released":true}},"state_digest":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba","status":"ok","turn":14,"unsure":[],"why":"baseline (no live evidence)"},"state":{"digest":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,true,false,false,false]},"mood":{"arousal":[0,0,0],"valence":[0.342003,0,-0.342003]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"none","initiative":"suggest","valence":"even","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":15}} +{"deviation":{"input":1.6857,"pred":1.7805,"self":2.8308},"inputs":{"elapsed_hours":0.5,"error":true},"n":16,"prev":"sha256:05d854ca158ab2d6fe5b310d0d4b785703556925e175ac591520710d908ee9b4","stance":"sha256:14490dfe573bbd90bc91901e7372278758d1c8c9bcbbf8b45a4c50e03f04f0a9","state":"sha256:3ce4d0cd4fcc67529de310a5b07ea0f348b24d4834362906eed981f4490bd3b3"} +{"deviation":{"input":1.4897,"pred":0.77684,"self":0.21961},"inputs":{"elapsed_hours":1,"praise":true},"n":17,"prev":"sha256:df2efee5cc1f4a517cc77a8bd56c30d99c46e0e5344d863e64ec3cdb1ab3389f","stance":"sha256:d5a39913357d01edeed04e798e16730dafe97ecb406031501e32ffa33d3cba76","state":"sha256:5a2e0ed7a588a78f8bb697bf11bc9a241e74bfd043976ee1a77e2a0de965edb3"} +{"deviation":{"input":1.5368,"pred":1.6188,"self":2.158},"inputs":{"elapsed_hours":2,"loss":true},"n":18,"prev":"sha256:9c9e0bd9e30eb4cf3a60f011d3a76ef9c788e2ed726c8afe5344b9158c5edd2b","stance":"sha256:f759e0c8adc25d43e6dc138c3cfed372c5a8016615ea1c23a6b65827c53939dc","state":"sha256:11816113854730ab1bd7091c290bc0f20ca9a870513e4437511bb9a17bcf179b"} +{"deviation":{"input":1.3166,"pred":0.20393,"self":0},"inputs":{"elapsed_hours":13},"n":19,"prev":"sha256:171d3e3fe1407a4df0b1adb83b70673976aca386ccc033de8f0cd375f04a6217","stance":"sha256:7e720363c8afa260bb42a0a8c105ab20cae1fa30761f8f38aaa9a37c9525bebd","state":"sha256:fc62ce1c64e52ce54c11951280a068f335f5f7a746285fae51cd26e81ad9ddf5"} +{"deviation":{"input":1.6221,"pred":1.7338,"self":2.8333},"inputs":{"elapsed_hours":1,"error":true},"n":20,"prev":"sha256:654d45575a01362b56990acc0f64f2e9d9f236d30d226ade9b3651da34582a2d","stance":"sha256:131e8d2319a49d677e53a0d596829aa67fabee60e4fc50b8063d18210f4c6364","state":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e"} +{"checkpoint":20,"prev":"sha256:fa0af32af76d00d7877ea4dd78c798b4f896c19eae6f790596ec6f932395f0f1","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.622062690482329,"pred":1.7338026105253461,"registers":true,"self":2.8332527443993802,"z":1.791988},"engine":{"program":"sha256:a001a7625ac3d6497dc19537b9303245237d4ac0312e696da127822fb30ff2f0","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["after_error_check","flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":true,"errors_in_row":1,"loss":false,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: shorter, double-check; a short explanation; no jokes; double-check every step; warm and encouraging; suggest what to try next.","line_tokens":34,"mood":{"arousal":{"level":"steady","odds":{"calm":0.210437,"keyed":0.406074,"steady":0.383489},"p":0.383489,"released":true},"valence":{"level":"even","odds":{"down":0.280285,"even":0.507669,"up":0.212046},"p":0.507669,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"careful","odds":{"bold":0,"careful":1,"measured":0},"p":1,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.526402,"focused":0.108794,"open":0.364804},"p":0.526402,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.113298,"rich":0.466202,"sparse":0.4205},"p":0.466202,"released":true},"formality":{"level":"casual","odds":{"casual":0.705532,"formal":0.004117,"neutral":0.29035},"p":0.705532,"released":true},"humour":{"level":"none","odds":{"light":0.22708,"none":0.764258,"playful":0.008662},"p":0.764258,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"short","odds":{"full":0,"short":0.695554,"terse":0.304446},"p":0.695554,"released":true},"warmth":{"level":"warm","odds":{"cool":0.017612,"neutral":0.188021,"warm":0.794367},"p":0.794367,"released":true}},"state_digest":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e","status":"ok","turn":19,"unsure":[],"why":"my last answer was wrong -> caution up, valence down; my errors in a row 1 -> caution up, humour down"},"state":{"digest":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[true,false,false,false,true]},"mood":{"arousal":[0,0,0],"valence":[0.274294,0,-0.274294]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"careful","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"none","initiative":"suggest","valence":"even","verbosity":"short","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":20}} +{"deviation":{"input":1.4782,"pred":0.78713,"self":0.21706},"inputs":{"elapsed_hours":0.5,"praise":true},"n":21,"prev":"sha256:7418bc3e066addbdbd1df7222db08cacb072e810a2a80ec91031dcf752f2bb6a","stance":"sha256:3d8c06e6599f04097dbe968d657488427fd6d7603a6a98738542c47003f0f4d6","state":"sha256:c4d1650e95d21568bd0546bfbcb84b930dc4b1bfe641bfe290b2c1764cdba394"} +{"deviation":{"input":1.5148,"pred":1.61,"self":2.1741},"inputs":{"elapsed_hours":1,"loss":true},"n":22,"prev":"sha256:bcf23b1e2a97a32f3c6ea905bda81592492d6610b66674ee79fcafd7fbee7301","stance":"sha256:af9110913e09a3bd1e62f64d44374511f1c75c2badc642e4a9b444797eae17da","state":"sha256:fdf6a8088a9355df4983b0de03c4f0e1951d798d7afb3373b5a2fc39623c07b8"} diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml b/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml new file mode 100644 index 0000000..4a5f537 --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml @@ -0,0 +1,180 @@ +# The tutor (examples/persona/tutor.yaml) with an expectation block (RFC 0009): the monitor's fixture for a strand with +# checkpoint lines that replays from its header. The block is outside the persona digest and moves no stance. +# A fictional persona: a playful tutor. probbit persona format 1 (docs/persona.md). +probbit_persona: 1 +identity: + name: Pip + version: 1.1.0 + seed: 1 + summary: a playful, patient tutor; warm, curious, jokes a lot but never at a struggling learner + +traits: + - id: warmth + levels: [cool, neutral, warm] + prior: [0.05, 0.3, 0.65] + spread: 0.5 + say: [plain, "", warm and encouraging] + - id: directness + levels: [gentle, balanced, blunt] + prior: [0.45, 0.45, 0.1] + spread: 0.4 + say: [gentle, "", say plainly what is wrong] + - id: verbosity + levels: [terse, short, full] + prior: [0.1, 0.4, 0.5] + spread: 0.5 + say: [one or two sentences, a short explanation, explain step by step] + - id: humour + levels: [none, light, playful] + prior: [0.15, 0.45, 0.4] + spread: 0.6 + say: [no jokes, a light joke is welcome, playful] + - id: formality + levels: [casual, neutral, formal] + prior: [0.6, 0.35, 0.05] + spread: 0.3 + say: [casual, "", formal] + - id: initiative + levels: [follow, suggest, lead] + prior: [0.2, 0.5, 0.3] + spread: 0.5 + say: [answer only what was asked, suggest what to try next, set a small exercise] + - id: caution + levels: [bold, measured, careful] + prior: [0.25, 0.55, 0.2] + spread: 0.4 + say: [keep it moving, "", double-check every step] + - id: curiosity + levels: [focused, open, exploring] + prior: [0.15, 0.4, 0.45] + spread: 0.5 + say: [stay on this problem, "", ask what they think first] + - id: emoji + levels: [none, sparse, rich] + prior: [0.2, 0.5, 0.3] + spread: 0.5 + say: [no emoji, one emoji at most, emoji welcome] + +moods: + - id: valence + levels: [down, even, up] + prior: [0.1, 0.5, 0.4] + spread: 0.4 + inertia: 0.5 + half_life_hours: 12 + - id: arousal + levels: [calm, steady, keyed] + prior: [0.3, 0.4, 0.3] + spread: 0.3 + inertia: 0.4 + +couplings: + - {vars: [valence, humour], align: 1.0} + - {vars: [valence, emoji], align: 0.6} + - {vars: [arousal, verbosity], align: -0.4} + - {vars: [warmth, formality], align: -0.6} + - {vars: [humour, formality], align: -0.6} + - {vars: [curiosity, verbosity], align: 0.3} + - {vars: [humour, emoji], align: 0.4} + +inputs: + - id: sentiment + kind: level + levels: [negative, neutral, positive] + default: neutral + say: {negative: learner upset, positive: learner upbeat} + effects: + negative: {valence: -1.0, warmth: 0.6, humour: -1.0, directness: -0.6} + positive: {valence: 0.8, humour: 0.5, emoji: 0.3} + - id: confused + kind: flag + say: learner confused + effects: {verbosity: 0.9, humour: -0.6, warmth: 0.4, curiosity: 0.6, initiative: 0.3, directness: -0.3} + - id: error + kind: flag + say: my last answer was wrong + effects: {caution: 1.2, verbosity: -0.3, valence: -0.4, humour: -0.4} + - id: loss + kind: flag + say: learner reports a failure + effects: {valence: -1.6, warmth: 1.0, humour: -2.0, emoji: -0.8} + - id: praise + kind: flag + say: praise + reactivity_spread: 0.4 + effects: {valence: 1.2, humour: 0.5, emoji: 0.5} + - id: criticism + kind: flag + say: criticism + effects: {valence: -0.6, caution: 0.6, humour: -0.4} + - id: stakes + kind: number + say: stakes + effects: {caution: 1.4, humour: -0.8, emoji: -0.6} + - id: time_pressure + kind: number + say: time pressure + effects: {verbosity: -1.8, curiosity: -1.2, humour: -0.4, arousal: 0.6} + - id: claim_done + kind: flag + say: about to say it is solved + effects: {caution: 0.5, "step.check": 0.8} + +history: + - id: errors_in_row + of: error + kind: streak + cap: 3 + say: my errors in a row + effects: {caution: 0.5, humour: -0.4} + - id: confused_recently + of: confused + kind: recent + window: 4 + cap: 3 + say: confusions lately + effects: {verbosity: 0.3, warmth: 0.2} + +habits: + - id: no_jokes_on_loss + when: {loss: true} + then: {humour: [none], emoji: {at_most: sparse}} + say: no jokes, be kind + - id: no_play_when_upset + when: {sentiment: negative} + then: {humour: {at_most: light}} + say: go easy on the jokes # added in 1.1.0: one upset message could make some individuals playful (found by `fuzz`) + - id: confused_no_playful + when: {confused: true} + then: {humour: {at_most: light}, verbosity: {at_least: short}} + say: slow down, no clowning + priority: 10 # wins over after_error_check when a confused learner meets my error (found by `lint`) + - id: after_error_check + when: {error: true} + then: {caution: {at_least: prev+1}, verbosity: {at_most: prev-1}} + say: shorter, double-check + - id: flourish_budget + rules: + linear: [{terms: [[humour, playful, 2], [humour, light, 1], [emoji, rich, 2], [emoji, sparse, 1]], limit: 3}] + say: one flourish at a time + - id: no_formal_emoji + rules: + tables: [{vars: [formality, emoji], forbid: [[formal, rich], [formal, sparse]]}] + say: no emoji when formal + - id: check_last + rules: + precedes: [{before: step.explain, after: step.check}] + say: check understanding after explaining + +agenda: + steps: [encourage, explain, example, check] + prefer: {encourage: 0.5, explain: 0.3, example: -0.1, check: -0.6} + +engine: + on_conflict: yield # two habits that cannot both hold: the lower priority one yields, and the turn says so + +line: + max_tokens: 40 + order: [verbosity, humour, warmth, initiative, curiosity, emoji, directness, caution, formality] + +expectation: {} diff --git a/probbit-cli/tests/monitor.rs b/probbit-cli/tests/monitor.rs index 2ae9a3e..57a55c4 100644 --- a/probbit-cli/tests/monitor.rs +++ b/probbit-cli/tests/monitor.rs @@ -463,3 +463,39 @@ fn one_monitor_serves_many_strands() { assert_eq!(get(port, "/s/zeta/doc/1", &host).2, format!("{}\n", db[0])); drop(child); let _ = std::fs::remove_dir_all(&dir); } + +/// RFC 0009: a strand with checkpoint lines whose persona has an expectation block replays from its header (no checkpoint +/// carries the expectation's memory). The fixture is the life of `tutor-expect.yaml` (the tutor with an expectation block) over +/// `tutor-expect.events.jsonl` (22 events, seed 2, the fixed clock, a checkpoint after every 5th: 4 of them), as `probbit live +/// tests/fixtures/monitor/tutor-expect.yaml --seed 2 --clock fixed --events ... --checkpoint-every 5 --strand` writes it. `--once` draws event 22 and exits 0, `--serve` streams it, `--dir` +/// lists it with its n; the same events through the tutor (no expectation block) still start at the last checkpoint +#[test] +fn checkpointed_strands_of_an_expectation_persona_open() { + const F: &str = "tests/fixtures/monitor/tutor-expect.strand"; + const EV: &str = "tests/fixtures/monitor/tutor-expect.events.jsonl"; + assert_eq!(std::fs::read_to_string(F).unwrap().lines().filter(|l| l.starts_with(r#"{"checkpoint":"#)).count(), 4); + let (c, v, err) = probbit(&["live", "verify", F]); assert_eq!(c, 0, "{v}{err}"); assert!(v.contains(r#""checkpoints":4"#) && v.contains(r#""events":22"#), "{v}"); + let (c, out, err) = probbit(&["monitor", F, "--once", "--plain"]); + assert_eq!(c, 0, "{out}{err}"); let top = out.lines().next().unwrap_or(""); + assert!(top.contains("| event 22 |") && top.ends_with("replay verified [ok]"), "from the header: {top}"); + // the page: the latest frame on /events is event 22, replayed from the header + let (child, port) = served(&[F]); let host = format!("127.0.0.1:{port}"); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); s.set_read_timeout(Some(Duration::from_secs(20))).unwrap(); + write!(s, "GET /events HTTP/1.1\r\nHost: {host}\r\n\r\n").unwrap(); + let mut buf = String::new(); let evs = stream(&mut s, &mut buf, 2); + assert_eq!(evs[1].0, "frame"); assert!(evs[1].1.contains(r#""from_checkpoint":null"#) && evs[1].1.contains(r#""n":22,"#) && evs[1].1.contains(r#""diverges":null"#), "{}", evs[1].1); + drop(s); drop(child); + // --dir: listed with its n + let dir = std::env::temp_dir().join(format!("probbit-monitor-expect-dir-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); + std::fs::copy(F, dir.join("tutor-expect.strand")).unwrap(); + let (child, port) = served(&["--dir", &dir.to_string_lossy()]); let host = format!("127.0.0.1:{port}"); + let t = Instant::now(); let mut list = String::new(); + while !list.contains(r#""n":22,"#) { assert!(t.elapsed() < Duration::from_secs(10), "listed with n 22: {list}"); std::thread::sleep(Duration::from_millis(100)); list = get(port, "/strands", &host).2; } + assert!(list.contains(r#""id":"tutor-expect""#) && list.contains(r#""persona":"Pip""#), "{list}"); + drop(child); let _ = std::fs::remove_dir_all(&dir); + // a persona without an expectation block: the shortcut (the badge names the checkpoint the replay started from) + let tutor = tmp("expect-tutor"); lived(&tutor, "2", &["--checkpoint-every", "5"], &std::fs::read_to_string(EV).unwrap()); + let (c, out, err) = probbit(&["monitor", &tutor, "--once", "--plain"]); let top = out.lines().next().unwrap_or(""); + assert_eq!(c, 0, "{out}{err}"); assert!(top.contains("| event 22 |") && top.ends_with("replay verified from checkpoint 20 [ok]"), "{top}"); + let _ = std::fs::remove_file(&tutor); +}