bxcore: a turn's speaker, SessionUpdated's turn, the session-state messages
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -88,7 +88,7 @@ impl Broker {
|
||||
}
|
||||
}
|
||||
|
||||
/// The snake_case wire name of a message: all twenty-six kinds, none with a leading underscore.
|
||||
/// The snake_case wire name of a message: all twenty-eight kinds, none with a leading underscore.
|
||||
pub fn kind(msg: &Message) -> &'static str {
|
||||
match msg {
|
||||
Message::ToolRequest(_) => "tool_request",
|
||||
@@ -117,6 +117,8 @@ pub fn kind(msg: &Message) -> &'static str {
|
||||
Message::Reindexed(_) => "reindexed",
|
||||
Message::MemoryAdmin(_) => "memory_admin",
|
||||
Message::MemoryAdminReply(_) => "memory_admin_reply",
|
||||
Message::SessionState(_) => "session_state",
|
||||
Message::SessionStateReply(_) => "session_state_reply",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ use brokerd::ledger::NOT_RECORDED;
|
||||
use brokerd::runner::{REFUSING, RunError};
|
||||
use bxcore::{
|
||||
ApprovalList, Approve, ApproveResult, AuditEvent, DataClass, DecisionRecord, DenyReason, Empty,
|
||||
Envelope, ErrorCode, GrantsReport, Message, Refuse, ResultStatus, SessionId, ToolResponse,
|
||||
Turn, TurnDone, TurnEvent, Usage, WireError,
|
||||
Envelope, ErrorCode, GrantsReport, Message, Refuse, ResultStatus, SessionId, SessionState,
|
||||
SessionStateReply, ToolResponse, Turn, TurnDone, TurnEvent, Usage, WireError,
|
||||
};
|
||||
use client::{Serve, call, next, open};
|
||||
use rig::{Rig, grant_text, request};
|
||||
@@ -158,6 +158,7 @@ fn every_kind_but_tool_request() -> Vec<Message> {
|
||||
session: SessionId::new("s1").unwrap(),
|
||||
content: String::new(),
|
||||
resume: false,
|
||||
speaker: None,
|
||||
}),
|
||||
Message::TurnEvent(TurnEvent::Content {
|
||||
text: String::new(),
|
||||
@@ -217,6 +218,24 @@ fn every_other_kind_on_broker_sock_is_forbidden() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A refused kind is logged by its wire name, the new session-state messages too (M5c spec,
|
||||
/// section 7).
|
||||
#[test]
|
||||
fn the_session_state_kinds_have_their_wire_names() {
|
||||
for msg in [
|
||||
Message::SessionState(SessionState {
|
||||
session: SessionId::new("s1").unwrap(),
|
||||
}),
|
||||
Message::SessionStateReply(SessionStateReply {
|
||||
class: DataClass::Private,
|
||||
untrusted: false,
|
||||
}),
|
||||
] {
|
||||
let wire = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(Some(broker::kind(&msg)), wire["kind"].as_str(), "{wire}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_that_is_not_json_is_answered_with_bad_message() {
|
||||
let rig = Rig::new("broker-badframe");
|
||||
|
||||
@@ -38,7 +38,8 @@ pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError};
|
||||
pub use log::{LogRecord, ToolCall, Usage};
|
||||
pub use memory::{
|
||||
ConsolidateEntry, EntryAction, MemoryAdmin, MemoryAdminReply, MemoryArgs, MemoryBlockReply,
|
||||
MemoryCall, MemoryCommand, MemoryFile, RecallArgs, Reindexed, SessionUpdated,
|
||||
MemoryCall, MemoryCommand, MemoryFile, RecallArgs, Reindexed, SessionState, SessionStateReply,
|
||||
SessionUpdated,
|
||||
};
|
||||
pub use wire::{
|
||||
ApprovalList, Approve, ApproveResult, DenyReason, Empty, Envelope, ErrorCode, GrantProblem,
|
||||
|
||||
@@ -37,6 +37,10 @@ pub enum LogRecord {
|
||||
User {
|
||||
time: Timestamp,
|
||||
content: String,
|
||||
/// The turn's speaker (M5c spec, section 11). Written only when present, so a log written
|
||||
/// before M5c reads as it is, and a turn without a speaker is logged as before.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
speaker: Option<String>,
|
||||
},
|
||||
Assistant {
|
||||
time: Timestamp,
|
||||
|
||||
@@ -228,11 +228,30 @@ pub struct MemoryBlockReply {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// `loopd` → `memoryd`: this session's log has new records.
|
||||
/// `loopd` → `memoryd`: this session's log has new records, those of a turn that has ended.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SessionUpdated {
|
||||
pub session: SessionId,
|
||||
/// The ended turn's number in the session (M5c spec, section 5). It has no default: a frame
|
||||
/// without it is refused, never read as turn 0.
|
||||
pub turn: u64,
|
||||
}
|
||||
|
||||
/// `memoryd` → `brokerd`, on `state.sock`: the session's state now (M5c spec, section 7).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SessionState {
|
||||
pub session: SessionId,
|
||||
}
|
||||
|
||||
/// `brokerd` → `memoryd`: the session's class, and whether it has read untrusted content. Neither
|
||||
/// field has a default: a reply without one is refused, never read as a clean session.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SessionStateReply {
|
||||
pub class: DataClass,
|
||||
pub untrusted: bool,
|
||||
}
|
||||
|
||||
/// `brokerd` → `memoryd`: a decided `recall` or `memory` call, with the session's state when it
|
||||
|
||||
@@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::gateway::{ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest};
|
||||
use crate::memory::{
|
||||
MemoryAdmin, MemoryAdminReply, MemoryBlockReply, MemoryCall, Reindexed, SessionUpdated,
|
||||
MemoryAdmin, MemoryAdminReply, MemoryBlockReply, MemoryCall, Reindexed, SessionState,
|
||||
SessionStateReply, SessionUpdated,
|
||||
};
|
||||
use crate::{CallId, DataClass, DecisionRecord, SessionId, Timestamp, Usage};
|
||||
|
||||
@@ -51,6 +52,8 @@ pub enum Message {
|
||||
Reindexed(Reindexed),
|
||||
MemoryAdmin(MemoryAdmin),
|
||||
MemoryAdminReply(MemoryAdminReply),
|
||||
SessionState(SessionState),
|
||||
SessionStateReply(SessionStateReply),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -126,13 +129,18 @@ pub enum DenyReason {
|
||||
StateUnreadable,
|
||||
}
|
||||
|
||||
// JSON: {"kind":"turn","body":{"session":"…","content":"…","resume":false}}
|
||||
// JSON: {"kind":"turn","body":{"session":"…","content":"…","resume":false,"speaker":"kyle"}}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Turn {
|
||||
pub session: SessionId,
|
||||
pub content: String,
|
||||
pub resume: bool,
|
||||
/// Who spoke the turn, when one person did (M5c spec, section 11). Written only when present,
|
||||
/// and a frame without it reads as `None`, the safe side: a turn without a speaker is never
|
||||
/// synced to Honcho.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub speaker: Option<String>,
|
||||
}
|
||||
|
||||
// JSON: {"event":"content","text":"…"} — the tag sits beside the fields, snake_case
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"type":"session_start","time":"2026-09-27T09:00:00.000Z","session":"mm-r0000000000000000000000001","epoch":0,"slot":2,"baseline":"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"}
|
||||
{"type":"user","time":"2026-09-27T09:00:01.000Z","content":"When is the launch?","speaker":"kyle"}
|
||||
{"type":"assistant","time":"2026-09-27T09:00:04.000Z","content":"On Thursday.","reasoning_content":null,"tool_calls":[]}
|
||||
{"type":"user","time":"2026-09-27T09:01:00.000Z","content":"And the review?"}
|
||||
{"type":"assistant","time":"2026-09-27T09:01:03.000Z","content":"On Friday.","reasoning_content":null,"tool_calls":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1,"id":1,"final":true,"msg":{"kind":"session_state","body":{"session":"mm-r0000000000000000000000001"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1,"id":1,"final":true,"msg":{"kind":"session_state_reply","body":{"class":"private","untrusted":true}}}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"v":1,"id":1,"final":true,"msg":{"kind":"session_updated","body":{"session":"mm-r0000000000000000000000001"}}}
|
||||
{"v":1,"id":1,"final":true,"msg":{"kind":"session_updated","body":{"session":"mm-r0000000000000000000000001","turn":3}}}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1,"id":3,"final":true,"msg":{"kind":"turn","body":{"session":"mm-r0000000000000000000000001","content":"What time is it?","resume":true,"speaker":"kyle"}}}
|
||||
@@ -1,9 +1,11 @@
|
||||
//! The eight memory messages against byte-exact fixtures, each refusing an unknown key (M5a spec,
|
||||
//! section 7). Do not edit these or the fixtures.
|
||||
//! The memory messages against byte-exact fixtures, each refusing an unknown key: the eight of M5a
|
||||
//! (M5a spec, section 7) and the session-state question of M5c (M5c spec, section 7). Do not edit
|
||||
//! these or the fixtures.
|
||||
|
||||
use bxcore::{
|
||||
CallId, DataClass, Empty, Envelope, MemoryAdmin, MemoryAdminReply, MemoryBlockReply,
|
||||
MemoryCall, MemoryCommand, Message, Reindexed, SessionId, SessionUpdated, ToolRequest,
|
||||
MemoryCall, MemoryCommand, Message, Reindexed, SessionId, SessionState, SessionStateReply,
|
||||
SessionUpdated, ToolRequest,
|
||||
};
|
||||
|
||||
const SESSION: &str = "mm-r0000000000000000000000001";
|
||||
@@ -53,6 +55,7 @@ fn the_eight_messages() {
|
||||
1,
|
||||
Message::SessionUpdated(SessionUpdated {
|
||||
session: SessionId::new(SESSION).unwrap(),
|
||||
turn: 3,
|
||||
}),
|
||||
);
|
||||
check(
|
||||
@@ -96,6 +99,58 @@ fn the_eight_messages() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `turn` has no default: a `session_updated` without it, as a `loopd` from before M5c sent, is
|
||||
/// refused, never read as turn 0 (M5c spec, section 5).
|
||||
#[test]
|
||||
fn a_session_updated_without_turn_is_refused() {
|
||||
let old = r#"{"v":1,"id":1,"final":true,"msg":{"kind":"session_updated","body":{"session":"mm-r0000000000000000000000001"}}}"#;
|
||||
let error = serde_json::from_str::<Envelope>(old).unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("missing field `turn`"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `memoryd` asks `brokerd` for a session's state on `state.sock` (M5c spec, section 7).
|
||||
#[test]
|
||||
fn the_session_state_messages() {
|
||||
check(
|
||||
"session_state.json",
|
||||
1,
|
||||
Message::SessionState(SessionState {
|
||||
session: SessionId::new(SESSION).unwrap(),
|
||||
}),
|
||||
);
|
||||
check(
|
||||
"session_state_reply.json",
|
||||
1,
|
||||
Message::SessionStateReply(SessionStateReply {
|
||||
class: DataClass::Private,
|
||||
untrusted: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Neither field of the reply has a default: a reply without one is refused, never read as a
|
||||
/// clean or `public` session.
|
||||
#[test]
|
||||
fn a_session_state_reply_needs_both_fields() {
|
||||
let text = fixture("session_state_reply.json");
|
||||
for (field, without) in [
|
||||
("class", text.replacen(r#""class":"private","#, "", 1)),
|
||||
("untrusted", text.replacen(r#","untrusted":true"#, "", 1)),
|
||||
] {
|
||||
assert_ne!(without, text, "{field}: the fixture holds the field");
|
||||
let error = serde_json::from_str::<Envelope>(&without).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains(&format!("missing field `{field}`")),
|
||||
"{field}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_command_has_its_name() {
|
||||
for (command, name) in [
|
||||
|
||||
@@ -235,6 +235,7 @@ fn session_log_records() {
|
||||
LogRecord::User {
|
||||
time: ts("2026-09-17T08:05:01.000Z"),
|
||||
content: "What is in /etc/hosts?".to_string(),
|
||||
speaker: None,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts("2026-09-17T08:05:03.000Z"),
|
||||
@@ -275,6 +276,45 @@ fn session_log_records() {
|
||||
check("session.jsonl", &want);
|
||||
}
|
||||
|
||||
/// A user record holds its turn's speaker when the turn had one, written last; a record without
|
||||
/// one has the bytes it had before M5c, so a log written earlier reads as it is and is never
|
||||
/// rewritten (M5c spec, section 11).
|
||||
#[test]
|
||||
fn user_records_with_and_without_a_speaker() {
|
||||
let want = [
|
||||
LogRecord::SessionStart {
|
||||
time: ts("2026-09-27T09:00:00.000Z"),
|
||||
session: sid("mm-r0000000000000000000000001"),
|
||||
epoch: Epoch(0),
|
||||
slot: 2,
|
||||
baseline: Hash32::from_hex(SEQ_HEX).unwrap(),
|
||||
},
|
||||
LogRecord::User {
|
||||
time: ts("2026-09-27T09:00:01.000Z"),
|
||||
content: "When is the launch?".to_string(),
|
||||
speaker: Some("kyle".to_string()),
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts("2026-09-27T09:00:04.000Z"),
|
||||
content: Some("On Thursday.".to_string()),
|
||||
reasoning_content: None,
|
||||
tool_calls: vec![],
|
||||
},
|
||||
LogRecord::User {
|
||||
time: ts("2026-09-27T09:01:00.000Z"),
|
||||
content: "And the review?".to_string(),
|
||||
speaker: None,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts("2026-09-27T09:01:03.000Z"),
|
||||
content: Some("On Friday.".to_string()),
|
||||
reasoning_content: None,
|
||||
tool_calls: vec![],
|
||||
},
|
||||
];
|
||||
check("session_speaker.jsonl", &want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_and_types_are_rejected() {
|
||||
let user = r#"{"type":"user","time":"2026-09-17T08:05:01.000Z","content":"hi"}"#;
|
||||
|
||||
@@ -92,6 +92,10 @@ fn envelopes_reject_unknown_keys_at_every_depth() {
|
||||
"error_no_such_approval.json",
|
||||
"turn_event_approval_pending.json",
|
||||
"turn_event_tool_denied.json",
|
||||
"turn_speaker.json",
|
||||
"session_updated.json",
|
||||
"session_state.json",
|
||||
"session_state_reply.json",
|
||||
] {
|
||||
// Envelope, msg and body: three objects. Some bodies hold more: turn_done a usage
|
||||
// object, approval_list one item, approve_result an outcome, grants_report two problems.
|
||||
@@ -142,6 +146,16 @@ fn usage_log_records_reject_unknown_keys_at_every_depth() {
|
||||
assert_eq!(tried, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speaker_log_records_reject_unknown_keys_at_every_depth() {
|
||||
let mut tried = 0;
|
||||
for (i, line) in fixture("records/session_speaker.jsonl").lines().enumerate() {
|
||||
tried += check::<LogRecord>(&format!("session_speaker.jsonl:{}", i + 1), line);
|
||||
}
|
||||
// Five records, none holding another object.
|
||||
assert_eq!(tried, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grants_reject_unknown_keys_at_every_depth() {
|
||||
let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap();
|
||||
|
||||
@@ -46,16 +46,46 @@ fn usage() -> Usage {
|
||||
}
|
||||
}
|
||||
|
||||
/// A turn without a speaker has the bytes it had before M5c, and a frame without `speaker` reads
|
||||
/// as `None`: a turn with no speaker is never synced (M5c spec, section 4).
|
||||
#[test]
|
||||
fn turn() {
|
||||
let body = Turn {
|
||||
session: SessionId::new("chat-1789700000-42").unwrap(),
|
||||
content: "What time is it?".to_string(),
|
||||
resume: false,
|
||||
speaker: None,
|
||||
};
|
||||
check("turn.json", env(3, true, Message::Turn(body)));
|
||||
}
|
||||
|
||||
/// The speaker is written last, when there is one (M5c spec, section 11).
|
||||
#[test]
|
||||
fn turn_with_a_speaker() {
|
||||
let body = Turn {
|
||||
session: SessionId::new("mm-r0000000000000000000000001").unwrap(),
|
||||
content: "What time is it?".to_string(),
|
||||
resume: true,
|
||||
speaker: Some("kyle".to_string()),
|
||||
};
|
||||
check("turn_speaker.json", env(3, true, Message::Turn(body)));
|
||||
}
|
||||
|
||||
/// `bxcore` does not check a speaker's name: `memoryd` checks it against Honcho's pattern (M5c
|
||||
/// spec, section 3). A Mattermost username with a `.` is carried as it is.
|
||||
#[test]
|
||||
fn a_speaker_is_carried_as_it_is() {
|
||||
let body = Turn {
|
||||
session: SessionId::new("mm-r0000000000000000000000001").unwrap(),
|
||||
content: "hi".to_string(),
|
||||
resume: false,
|
||||
speaker: Some("first.last".to_string()),
|
||||
};
|
||||
let text = serde_json::to_string(&body).unwrap();
|
||||
assert!(text.ends_with(r#","speaker":"first.last"}"#), "{text}");
|
||||
assert_eq!(serde_json::from_str::<Turn>(&text).unwrap(), body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_events() {
|
||||
check(
|
||||
@@ -262,6 +292,7 @@ fn usage_records() {
|
||||
LogRecord::User {
|
||||
time: Timestamp::parse("2026-09-18T08:05:01.000Z").unwrap(),
|
||||
content: "What time is it?".to_string(),
|
||||
speaker: None,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: Timestamp::parse("2026-09-18T08:05:03.000Z").unwrap(),
|
||||
|
||||
@@ -87,6 +87,8 @@ pub fn run_turn(
|
||||
session: session.clone(),
|
||||
content: content.to_string(),
|
||||
resume,
|
||||
// Set in M5c-1 task 04, from `--speaker`. Until then no turn has a speaker.
|
||||
speaker: None,
|
||||
};
|
||||
write_frame(
|
||||
&mut stream,
|
||||
|
||||
@@ -174,7 +174,8 @@ fn run_turn_delivers_every_event_in_order_then_the_answer() {
|
||||
Turn {
|
||||
session: id("s1"),
|
||||
content: "what time is it?".to_string(),
|
||||
resume: false
|
||||
resume: false,
|
||||
speaker: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ fn one_turn(
|
||||
session: batch.session.clone(),
|
||||
content: batch.text.clone(),
|
||||
resume,
|
||||
// Set in M5c-1 task 03. Until then no turn has a speaker, and none is synced.
|
||||
speaker: None,
|
||||
};
|
||||
if let Err(e) = write_frame(
|
||||
&mut stream,
|
||||
|
||||
@@ -94,6 +94,8 @@ impl MemorySource for MemoryPort {
|
||||
fn updated(&self, session: &SessionId) -> Result<(), String> {
|
||||
match self.ask(Message::SessionUpdated(SessionUpdated {
|
||||
session: session.clone(),
|
||||
// Set in M5c-1 task 02. `memoryd` does not read it before M5c-1 task 09.
|
||||
turn: 0,
|
||||
}))? {
|
||||
Message::Ok(_) => Ok(()),
|
||||
Message::Error(e) => Err(format!("memoryd: {}", e.detail)),
|
||||
|
||||
@@ -123,6 +123,9 @@ pub fn run_turn(
|
||||
.append(LogRecord::User {
|
||||
time: Timestamp::now(),
|
||||
content: neutralise(content).into_owned(),
|
||||
// Set in M5c-1 task 02, from the turn. `None` is not written: the record's bytes
|
||||
// are as before.
|
||||
speaker: None,
|
||||
})
|
||||
.map_err(TurnError::Session)?;
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ fn replay_keeps_only_messages_in_order_and_unchanged() {
|
||||
LogRecord::User {
|
||||
time: ts(),
|
||||
content: "hi".to_string(),
|
||||
speaker: None,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts(),
|
||||
@@ -195,6 +196,7 @@ fn replay_of_a_prefix_is_a_prefix() {
|
||||
0 => LogRecord::User {
|
||||
time: ts(),
|
||||
content: format!("u{i}"),
|
||||
speaker: None,
|
||||
},
|
||||
1 => LogRecord::Assistant {
|
||||
time: ts(),
|
||||
|
||||
@@ -65,6 +65,7 @@ fn turn(socket: &PathBuf, session: &str, content: &str, resume: bool) -> Vec<Env
|
||||
session: id(session),
|
||||
content: content.to_string(),
|
||||
resume,
|
||||
speaker: None,
|
||||
});
|
||||
write_frame(
|
||||
&mut stream,
|
||||
@@ -350,6 +351,7 @@ fn bad_frames_get_an_error_and_a_close() {
|
||||
session: id("a"),
|
||||
content: "x".to_string(),
|
||||
resume: false,
|
||||
speaker: None,
|
||||
});
|
||||
write_frame(
|
||||
&mut stream,
|
||||
|
||||
@@ -153,6 +153,7 @@ fn answers(socket: &Path) -> bool {
|
||||
session: SessionId::new("probe").unwrap(),
|
||||
content: "are you there".to_string(),
|
||||
resume: true,
|
||||
speaker: None,
|
||||
}),
|
||||
};
|
||||
if write_frame(&mut stream, &turn).is_err() {
|
||||
|
||||
@@ -88,6 +88,7 @@ fn turn(l: &Loopd, session: &str, resume: bool) -> Message {
|
||||
session: SessionId::new(session).unwrap(),
|
||||
content: "hello".to_string(),
|
||||
resume,
|
||||
speaker: None,
|
||||
});
|
||||
let env = Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
|
||||
@@ -125,6 +125,7 @@ fn an_old_log_with_a_raw_tag_is_replayed_as_it_is() {
|
||||
LogRecord::User {
|
||||
time: ts(),
|
||||
content: user.clone(),
|
||||
speaker: None,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts(),
|
||||
|
||||
@@ -62,6 +62,7 @@ fn append_is_visible_on_disk_at_once_and_after_reopen() {
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "hi\nthere \"quoted\" caf\u{e9}".to_string(),
|
||||
speaker: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -104,6 +105,7 @@ fn a_torn_log_is_refused_with_the_line_number() {
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "hi".to_string(),
|
||||
speaker: None,
|
||||
})
|
||||
.unwrap();
|
||||
drop(s);
|
||||
@@ -173,6 +175,7 @@ fn last_usage_is_the_latest_usage_record() {
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "x".to_string(),
|
||||
speaker: None,
|
||||
})
|
||||
.unwrap();
|
||||
s.append(LogRecord::Usage {
|
||||
|
||||
@@ -74,6 +74,7 @@ fn untrusted(socket: &Path, session: &str, content: &str, resume: bool) -> bool
|
||||
session: SessionId::new(session).unwrap(),
|
||||
content: content.to_string(),
|
||||
resume,
|
||||
speaker: None,
|
||||
});
|
||||
let turn = Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
|
||||
@@ -221,7 +221,7 @@ impl Index {
|
||||
return Ok(());
|
||||
}
|
||||
match record {
|
||||
LogRecord::User { time, content } if !content.trim().is_empty() => {
|
||||
LogRecord::User { time, content, .. } if !content.trim().is_empty() => {
|
||||
let src = Source {
|
||||
time: time.to_rfc3339(),
|
||||
role: "owner".to_string(),
|
||||
|
||||
@@ -143,6 +143,7 @@ fn recall_finds_old_sessions_updated_ones_and_notes() {
|
||||
append(&log, &user(2, "the launch is Thursday"));
|
||||
let update = Message::SessionUpdated(SessionUpdated {
|
||||
session: SessionId::new("mm-b").unwrap(),
|
||||
turn: 1,
|
||||
});
|
||||
assert_eq!(ask(&r.config.loop_socket(), update), Message::Ok(Empty {}));
|
||||
call(
|
||||
|
||||
@@ -14,6 +14,7 @@ pub fn user(minute: u32, content: &str) -> LogRecord {
|
||||
LogRecord::User {
|
||||
time: at(minute),
|
||||
content: content.to_string(),
|
||||
speaker: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user