1use super::{ProtocolEvent, ProtocolEventMeta};
2use crate::message::TranscriptMessage;
3
4macro_rules! match_event_meta {
5 ($event:expr, |$session_id:ident, $seq:ident, $created_at:ident| $body:expr) => {
6 match $event {
7 ProtocolEvent::SessionStarted {
8 session_id: $session_id,
9 seq: $seq,
10 created_at: $created_at,
11 ..
12 }
13 | ProtocolEvent::SessionTitleUpdated {
14 session_id: $session_id,
15 seq: $seq,
16 created_at: $created_at,
17 ..
18 }
19 | ProtocolEvent::AssistantDelta {
20 session_id: $session_id,
21 seq: $seq,
22 created_at: $created_at,
23 ..
24 }
25 | ProtocolEvent::AssistantMessageCompleted {
26 session_id: $session_id,
27 seq: $seq,
28 created_at: $created_at,
29 ..
30 }
31 | ProtocolEvent::ToolCallStarted {
32 session_id: $session_id,
33 seq: $seq,
34 created_at: $created_at,
35 ..
36 }
37 | ProtocolEvent::ToolApprovalRequested {
38 session_id: $session_id,
39 seq: $seq,
40 created_at: $created_at,
41 ..
42 }
43 | ProtocolEvent::ToolApprovalResolved {
44 session_id: $session_id,
45 seq: $seq,
46 created_at: $created_at,
47 ..
48 }
49 | ProtocolEvent::ToolResultAdded {
50 session_id: $session_id,
51 seq: $seq,
52 created_at: $created_at,
53 ..
54 }
55 | ProtocolEvent::SessionTurnFinished {
56 session_id: $session_id,
57 seq: $seq,
58 created_at: $created_at,
59 ..
60 }
61 | ProtocolEvent::StreamResyncRequired {
62 session_id: $session_id,
63 seq: $seq,
64 created_at: $created_at,
65 ..
66 }
67 | ProtocolEvent::Overloaded {
68 session_id: $session_id,
69 seq: $seq,
70 created_at: $created_at,
71 ..
72 }
73 | ProtocolEvent::PersistenceFailed {
74 session_id: $session_id,
75 seq: $seq,
76 created_at: $created_at,
77 ..
78 }
79 | ProtocolEvent::ProviderRetrying {
80 session_id: $session_id,
81 seq: $seq,
82 created_at: $created_at,
83 ..
84 }
85 | ProtocolEvent::SessionCompactionStarted {
86 session_id: $session_id,
87 seq: $seq,
88 created_at: $created_at,
89 ..
90 }
91 | ProtocolEvent::VerificationFinished {
92 session_id: $session_id,
93 seq: $seq,
94 created_at: $created_at,
95 ..
96 }
97 | ProtocolEvent::TaskGraphUpdated {
98 session_id: $session_id,
99 seq: $seq,
100 created_at: $created_at,
101 ..
102 }
103 | ProtocolEvent::TaskStatusUpdated {
104 session_id: $session_id,
105 seq: $seq,
106 created_at: $created_at,
107 ..
108 }
109 | ProtocolEvent::TaskArtifactUpdated {
110 session_id: $session_id,
111 seq: $seq,
112 created_at: $created_at,
113 ..
114 }
115 | ProtocolEvent::ArtifactLineageUpdated {
116 session_id: $session_id,
117 seq: $seq,
118 created_at: $created_at,
119 ..
120 }
121 | ProtocolEvent::RunInterrupted {
122 session_id: $session_id,
123 seq: $seq,
124 created_at: $created_at,
125 ..
126 }
127 | ProtocolEvent::RunFailed {
128 session_id: $session_id,
129 seq: $seq,
130 created_at: $created_at,
131 ..
132 }
133 | ProtocolEvent::SessionCompacted {
134 session_id: $session_id,
135 seq: $seq,
136 created_at: $created_at,
137 ..
138 }
139 | ProtocolEvent::SessionContextFolded {
140 session_id: $session_id,
141 seq: $seq,
142 created_at: $created_at,
143 ..
144 }
145 | ProtocolEvent::UsageUpdated {
146 session_id: $session_id,
147 seq: $seq,
148 created_at: $created_at,
149 ..
150 }
151 | ProtocolEvent::TurnUsage {
152 session_id: $session_id,
153 seq: $seq,
154 created_at: $created_at,
155 ..
156 }
157 | ProtocolEvent::PromptCacheBreak {
158 session_id: $session_id,
159 seq: $seq,
160 created_at: $created_at,
161 ..
162 }
163 | ProtocolEvent::ToolOutputDelta {
164 session_id: $session_id,
165 seq: $seq,
166 created_at: $created_at,
167 ..
168 }
169 | ProtocolEvent::BackgroundShellExited {
170 session_id: $session_id,
171 seq: $seq,
172 created_at: $created_at,
173 ..
174 }
175 | ProtocolEvent::BudgetExceeded {
176 session_id: $session_id,
177 seq: $seq,
178 created_at: $created_at,
179 ..
180 }
181 | ProtocolEvent::ProviderRouteUpdated {
182 session_id: $session_id,
183 seq: $seq,
184 created_at: $created_at,
185 ..
186 }
187 | ProtocolEvent::AccountRateLimited {
188 session_id: $session_id,
189 seq: $seq,
190 created_at: $created_at,
191 ..
192 }
193 | ProtocolEvent::AccountUsageReported {
194 session_id: $session_id,
195 seq: $seq,
196 created_at: $created_at,
197 ..
198 }
199 | ProtocolEvent::DispatchDecided {
200 session_id: $session_id,
201 seq: $seq,
202 created_at: $created_at,
203 ..
204 }
205 | ProtocolEvent::OrchestrationObservationRecorded {
206 session_id: $session_id,
207 seq: $seq,
208 created_at: $created_at,
209 ..
210 }
211 | ProtocolEvent::AgentQueryIssued {
212 session_id: $session_id,
213 seq: $seq,
214 created_at: $created_at,
215 ..
216 }
217 | ProtocolEvent::AgentQueryResponded {
218 session_id: $session_id,
219 seq: $seq,
220 created_at: $created_at,
221 ..
222 }
223 | ProtocolEvent::HumanSteeringInjected {
224 session_id: $session_id,
225 seq: $seq,
226 created_at: $created_at,
227 ..
228 }
229 | ProtocolEvent::SubAgentSpawned {
230 session_id: $session_id,
231 seq: $seq,
232 created_at: $created_at,
233 ..
234 }
235 | ProtocolEvent::SubAgentCompleted {
236 session_id: $session_id,
237 seq: $seq,
238 created_at: $created_at,
239 ..
240 }
241 | ProtocolEvent::SubAgentCancelled {
242 session_id: $session_id,
243 seq: $seq,
244 created_at: $created_at,
245 ..
246 }
247 | ProtocolEvent::UserMessageSubmitted {
248 session_id: $session_id,
249 seq: $seq,
250 created_at: $created_at,
251 ..
252 }
253 | ProtocolEvent::UserQuestionAsked {
254 session_id: $session_id,
255 seq: $seq,
256 created_at: $created_at,
257 ..
258 }
259 | ProtocolEvent::McpElicitationRequested {
260 session_id: $session_id,
261 seq: $seq,
262 created_at: $created_at,
263 ..
264 }
265 | ProtocolEvent::CredentialRequested {
266 session_id: $session_id,
267 seq: $seq,
268 created_at: $created_at,
269 ..
270 }
271 | ProtocolEvent::CredentialResolved {
272 session_id: $session_id,
273 seq: $seq,
274 created_at: $created_at,
275 ..
276 }
277 | ProtocolEvent::UserQuestionAnswered {
278 session_id: $session_id,
279 seq: $seq,
280 created_at: $created_at,
281 ..
282 }
283 | ProtocolEvent::PlanPresented {
284 session_id: $session_id,
285 seq: $seq,
286 created_at: $created_at,
287 ..
288 }
289 | ProtocolEvent::AgentRosterUpdated {
290 session_id: $session_id,
291 seq: $seq,
292 created_at: $created_at,
293 ..
294 }
295 | ProtocolEvent::CheckRunCompleted {
296 session_id: $session_id,
297 seq: $seq,
298 created_at: $created_at,
299 ..
300 }
301 | ProtocolEvent::GoalProgressUpdated {
302 session_id: $session_id,
303 seq: $seq,
304 created_at: $created_at,
305 ..
306 }
307 | ProtocolEvent::SelfReviewProposalReceived {
308 session_id: $session_id,
309 seq: $seq,
310 created_at: $created_at,
311 ..
312 }
313 | ProtocolEvent::ToolApprovalReviewed {
314 session_id: $session_id,
315 seq: $seq,
316 created_at: $created_at,
317 ..
318 }
319 | ProtocolEvent::ModelRouteDecision {
320 session_id: $session_id,
321 seq: $seq,
322 created_at: $created_at,
323 ..
324 }
325 | ProtocolEvent::RunGovernanceRejected {
326 session_id: $session_id,
327 seq: $seq,
328 created_at: $created_at,
329 ..
330 } => $body,
331 }
332 };
333}
334
335fn assign_seq(event: &mut ProtocolEvent, new_seq: i64) {
341 match event {
342 ProtocolEvent::SessionStarted { seq, .. }
343 | ProtocolEvent::SessionTitleUpdated { seq, .. }
344 | ProtocolEvent::AssistantDelta { seq, .. }
345 | ProtocolEvent::AssistantMessageCompleted { seq, .. }
346 | ProtocolEvent::ToolCallStarted { seq, .. }
347 | ProtocolEvent::ToolApprovalRequested { seq, .. }
348 | ProtocolEvent::ToolApprovalResolved { seq, .. }
349 | ProtocolEvent::ToolResultAdded { seq, .. }
350 | ProtocolEvent::SessionTurnFinished { seq, .. }
351 | ProtocolEvent::StreamResyncRequired { seq, .. }
352 | ProtocolEvent::Overloaded { seq, .. }
353 | ProtocolEvent::PersistenceFailed { seq, .. }
354 | ProtocolEvent::ProviderRetrying { seq, .. }
355 | ProtocolEvent::SessionCompactionStarted { seq, .. }
356 | ProtocolEvent::VerificationFinished { seq, .. }
357 | ProtocolEvent::TaskGraphUpdated { seq, .. }
358 | ProtocolEvent::TaskStatusUpdated { seq, .. }
359 | ProtocolEvent::TaskArtifactUpdated { seq, .. }
360 | ProtocolEvent::ArtifactLineageUpdated { seq, .. }
361 | ProtocolEvent::RunInterrupted { seq, .. }
362 | ProtocolEvent::RunFailed { seq, .. }
363 | ProtocolEvent::SessionCompacted { seq, .. }
364 | ProtocolEvent::SessionContextFolded { seq, .. }
365 | ProtocolEvent::UsageUpdated { seq, .. }
366 | ProtocolEvent::TurnUsage { seq, .. }
367 | ProtocolEvent::PromptCacheBreak { seq, .. }
368 | ProtocolEvent::ToolOutputDelta { seq, .. }
369 | ProtocolEvent::BackgroundShellExited { seq, .. }
370 | ProtocolEvent::BudgetExceeded { seq, .. }
371 | ProtocolEvent::ProviderRouteUpdated { seq, .. }
372 | ProtocolEvent::AccountRateLimited { seq, .. }
373 | ProtocolEvent::AccountUsageReported { seq, .. }
374 | ProtocolEvent::DispatchDecided { seq, .. }
375 | ProtocolEvent::OrchestrationObservationRecorded { seq, .. }
376 | ProtocolEvent::AgentQueryIssued { seq, .. }
377 | ProtocolEvent::AgentQueryResponded { seq, .. }
378 | ProtocolEvent::HumanSteeringInjected { seq, .. }
379 | ProtocolEvent::SubAgentSpawned { seq, .. }
380 | ProtocolEvent::SubAgentCompleted { seq, .. }
381 | ProtocolEvent::SubAgentCancelled { seq, .. }
382 | ProtocolEvent::UserMessageSubmitted { seq, .. }
383 | ProtocolEvent::UserQuestionAsked { seq, .. }
384 | ProtocolEvent::McpElicitationRequested { seq, .. }
385 | ProtocolEvent::CredentialRequested { seq, .. }
386 | ProtocolEvent::CredentialResolved { seq, .. }
387 | ProtocolEvent::UserQuestionAnswered { seq, .. }
388 | ProtocolEvent::PlanPresented { seq, .. }
389 | ProtocolEvent::AgentRosterUpdated { seq, .. }
390 | ProtocolEvent::CheckRunCompleted { seq, .. }
391 | ProtocolEvent::GoalProgressUpdated { seq, .. }
392 | ProtocolEvent::SelfReviewProposalReceived { seq, .. }
393 | ProtocolEvent::ModelRouteDecision { seq, .. }
394 | ProtocolEvent::ToolApprovalReviewed { seq, .. }
395 | ProtocolEvent::RunGovernanceRejected { seq, .. } => *seq = new_seq,
396 }
397}
398
399fn rewrite_message_id<F: FnOnce(&mut TranscriptMessage)>(event: &mut ProtocolEvent, rewrite: F) {
403 match event {
404 ProtocolEvent::AssistantMessageCompleted { message, .. } => rewrite(message),
405 ProtocolEvent::ToolResultAdded { result, .. } => rewrite(result),
406 ProtocolEvent::SessionContextFolded { fold, .. } => rewrite(fold),
407 _ => {}
408 }
409}
410
411impl ProtocolEvent {
412 pub fn meta(&self) -> ProtocolEventMeta {
413 match_event_meta!(self, |session_id, seq, created_at| ProtocolEventMeta {
414 session_id: session_id.clone(),
415 seq: *seq,
416 created_at: created_at.clone(),
417 })
418 }
419
420 pub fn session_id(&self) -> &str {
421 match_event_meta!(self, |session_id, _seq, _created_at| session_id.as_str())
422 }
423
424 pub fn seq(&self) -> i64 {
425 match_event_meta!(self, |_session_id, seq, _created_at| *seq)
426 }
427
428 pub fn created_at(&self) -> &str {
429 match_event_meta!(self, |_session_id, _seq, created_at| created_at.as_str())
430 }
431
432 pub fn set_seq(&mut self, new_seq: i64) {
435 assign_seq(self, new_seq);
436 }
437
438 pub fn canonicalize_identity(&mut self, authoritative_seq: i64) {
443 assign_seq(self, authoritative_seq);
444 let outer = authoritative_seq;
445 rewrite_message_id(self, |message| {
446 message.set_id(derive_message_id(message, outer));
447 });
448 }
449}
450
451fn derive_message_id(message: &TranscriptMessage, row_seq: i64) -> String {
453 use TranscriptMessage::*;
454 match message {
455 System { .. } => crate::ids::message_id(row_seq),
456 User { .. } => crate::ids::message_id(row_seq),
457 Assistant { .. } => crate::ids::assistant_message_id(row_seq),
458 Tool { tool_call_id, .. } => crate::ids::tool_result_id(tool_call_id, row_seq),
459 Compact { .. } => crate::ids::message_id(row_seq),
460 Fold { .. } => crate::ids::message_id(row_seq),
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::message::TranscriptMessage;
468 use crate::tool::ToolCall;
469
470 fn assistant_with_id(id: &str) -> TranscriptMessage {
471 TranscriptMessage::Assistant {
472 id: id.into(),
473 created_at: "t".into(),
474 content: "hi".into(),
475 reasoning_content: None,
476 tool_calls: vec![],
477 pinned: false,
478 }
479 }
480
481 fn tool_message(id: &str) -> TranscriptMessage {
482 TranscriptMessage::Tool {
483 id: id.into(),
484 created_at: "t".into(),
485 tool_call_id: "tc1".into(),
486 summary: "ok".into(),
487 output: None,
488 trust: None,
489 is_error: false,
490 pinned: false,
491 process_id: None,
492 }
493 }
494
495 #[test]
496 fn set_seq_rewrites_every_variant() {
497 let mut ev = ProtocolEvent::AssistantDelta {
498 session_id: "s".into(),
499 seq: 0,
500 created_at: "t".into(),
501 text: "x".into(),
502 is_reasoning: false,
503 };
504 ev.set_seq(42);
505 assert_eq!(ev.seq(), 42);
506
507 let mut ev = ProtocolEvent::ModelRouteDecision {
508 session_id: "s".into(),
509 seq: 0,
510 created_at: "t".into(),
511 decision: crate::event::ModelRouteDecisionPayload {
512 agent_role: "default".into(),
513 model_id: "test-model".into(),
514 tier: "cheap".into(),
515 reason: "unit-test".into(),
516 signals_digest: "0".into(),
517 price_snapshot: crate::event::PriceSnapshot {
518 prompt_per_m: 0.0,
519 completion_per_m: 0.0,
520 source: "test".into(),
521 fetched_at: "t".into(),
522 hash: "h".into(),
523 },
524 est_cost_usd: 0.0,
525 schema_version: 1,
526 },
527 };
528 ev.set_seq(7);
529 assert_eq!(ev.seq(), 7);
530 }
531
532 #[test]
533 fn canonicalize_identity_assigns_seq_and_message_id() {
534 let mut ev = ProtocolEvent::AssistantMessageCompleted {
535 session_id: "s".into(),
536 seq: 0,
537 created_at: "t".into(),
538 message: assistant_with_id("final"),
539 };
540 ev.canonicalize_identity(11);
541 assert_eq!(ev.seq(), 11);
542 match &ev {
543 ProtocolEvent::AssistantMessageCompleted { message, .. } => {
544 assert_eq!(message.id(), "assistant:11");
545 }
546 _ => panic!("variant"),
547 }
548 }
549
550 #[test]
551 fn canonicalize_tool_result_uses_tool_call_id() {
552 let mut ev = ProtocolEvent::ToolResultAdded {
553 session_id: "s".into(),
554 seq: 0,
555 created_at: "t".into(),
556 result: tool_message("tr"),
557 };
558 ev.canonicalize_identity(22);
559 match &ev {
560 ProtocolEvent::ToolResultAdded { result, .. } => {
561 assert_eq!(result.id(), "tool:tc1:22");
562 }
563 _ => panic!("variant"),
564 }
565 }
566
567 #[test]
568 fn canonicalize_session_started_keeps_variants_without_a_message() {
569 let mut ev = ProtocolEvent::SessionStarted {
570 session_id: "s".into(),
571 seq: 0,
572 created_at: "t".into(),
573 title: None,
574 };
575 ev.canonicalize_identity(3);
576 assert_eq!(ev.seq(), 3);
577 assert_eq!(ev.session_id(), "s");
578 }
579
580 #[test]
581 fn set_seq_is_exhaustive_compile_check() {
582 fn all_arms(e: &mut ProtocolEvent, mutate: impl Fn(&mut i64)) {
589 match e {
590 ProtocolEvent::SessionStarted { seq, .. }
591 | ProtocolEvent::SessionTitleUpdated { seq, .. }
592 | ProtocolEvent::AssistantDelta { seq, .. }
593 | ProtocolEvent::AssistantMessageCompleted { seq, .. }
594 | ProtocolEvent::ToolCallStarted { seq, .. }
595 | ProtocolEvent::ToolApprovalRequested { seq, .. }
596 | ProtocolEvent::ToolApprovalResolved { seq, .. }
597 | ProtocolEvent::ToolResultAdded { seq, .. }
598 | ProtocolEvent::SessionTurnFinished { seq, .. }
599 | ProtocolEvent::StreamResyncRequired { seq, .. }
600 | ProtocolEvent::Overloaded { seq, .. }
601 | ProtocolEvent::PersistenceFailed { seq, .. }
602 | ProtocolEvent::ProviderRetrying { seq, .. }
603 | ProtocolEvent::SessionCompactionStarted { seq, .. }
604 | ProtocolEvent::VerificationFinished { seq, .. }
605 | ProtocolEvent::TaskGraphUpdated { seq, .. }
606 | ProtocolEvent::TaskStatusUpdated { seq, .. }
607 | ProtocolEvent::TaskArtifactUpdated { seq, .. }
608 | ProtocolEvent::ArtifactLineageUpdated { seq, .. }
609 | ProtocolEvent::RunInterrupted { seq, .. }
610 | ProtocolEvent::RunFailed { seq, .. }
611 | ProtocolEvent::SessionCompacted { seq, .. }
612 | ProtocolEvent::SessionContextFolded { seq, .. }
613 | ProtocolEvent::UsageUpdated { seq, .. }
614 | ProtocolEvent::TurnUsage { seq, .. }
615 | ProtocolEvent::PromptCacheBreak { seq, .. }
616 | ProtocolEvent::ToolOutputDelta { seq, .. }
617 | ProtocolEvent::BackgroundShellExited { seq, .. }
618 | ProtocolEvent::BudgetExceeded { seq, .. }
619 | ProtocolEvent::ProviderRouteUpdated { seq, .. }
620 | ProtocolEvent::AccountRateLimited { seq, .. }
621 | ProtocolEvent::AccountUsageReported { seq, .. }
622 | ProtocolEvent::DispatchDecided { seq, .. }
623 | ProtocolEvent::OrchestrationObservationRecorded { seq, .. }
624 | ProtocolEvent::AgentQueryIssued { seq, .. }
625 | ProtocolEvent::AgentQueryResponded { seq, .. }
626 | ProtocolEvent::HumanSteeringInjected { seq, .. }
627 | ProtocolEvent::SubAgentSpawned { seq, .. }
628 | ProtocolEvent::SubAgentCompleted { seq, .. }
629 | ProtocolEvent::SubAgentCancelled { seq, .. }
630 | ProtocolEvent::UserMessageSubmitted { seq, .. }
631 | ProtocolEvent::UserQuestionAsked { seq, .. }
632 | ProtocolEvent::McpElicitationRequested { seq, .. }
633 | ProtocolEvent::CredentialRequested { seq, .. }
634 | ProtocolEvent::CredentialResolved { seq, .. }
635 | ProtocolEvent::UserQuestionAnswered { seq, .. }
636 | ProtocolEvent::PlanPresented { seq, .. }
637 | ProtocolEvent::AgentRosterUpdated { seq, .. }
638 | ProtocolEvent::CheckRunCompleted { seq, .. }
639 | ProtocolEvent::GoalProgressUpdated { seq, .. }
640 | ProtocolEvent::SelfReviewProposalReceived { seq, .. }
641 | ProtocolEvent::ToolApprovalReviewed { seq, .. }
642 | ProtocolEvent::RunGovernanceRejected { seq, .. } => mutate(seq),
643 ProtocolEvent::ModelRouteDecision { seq, .. } => mutate(seq),
647 }
648 }
649 let mut ev = ProtocolEvent::ToolCallStarted {
650 session_id: "s".into(),
651 seq: 0,
652 created_at: "t".into(),
653 call: ToolCall {
654 id: "tc1".into(),
655 name: "read_file".into(),
656 arguments_json: "{}".into(),
657 },
658 };
659 all_arms(&mut ev, |s| *s = 9);
660 assert_eq!(ev.seq(), 9);
661 }
662}