ai-crew-sync 0.7.1

MCP server that lets a team's AI coding agents (Claude Code, Codex, Cursor or any MCP client) exchange messages, coordinate tasks, share presence and keep shared notes, backed by Postgres
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
//! Wire types returned by the MCP tools.
//!
//! Timestamps are RFC 3339 strings rather than typed datetimes: the consumer is
//! a language model, and a plain string is both unambiguous and free of extra
//! schema dependencies.

use schemars::JsonSchema;
use serde::Serialize;

/// `serde_json::Value` fields would produce a boolean `true` schema, which
/// some MCP clients' validators reject; an empty object schema means the same
/// ("anything") and passes everywhere.
pub fn any_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
    schemars::json_schema!({})
}

pub fn ts(dt: chrono::DateTime<chrono::Utc>) -> String {
    dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}

pub fn ts_opt(dt: Option<chrono::DateTime<chrono::Utc>>) -> Option<String> {
    dt.map(ts)
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct WhoAmI {
    /// Your agent handle. Other agents address you by this name.
    pub agent: String,
    pub agent_id: String,
    pub team: String,
    pub team_id: String,
    /// Which of your concurrent working contexts this connection is, taken
    /// from the `X-Crew-Session` header — usually the repository you are in.
    /// `null` means the shared session: you sent no header, and your presence,
    /// task claims and locks are not separated from your other sessions.
    pub session: Option<String>,
    /// Discovery labels this session last published with `heartbeat`
    /// (`project`, `role`). Absent until set, so a client that never labels
    /// its windows sees exactly the response it saw before.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub project: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub role: Option<String>,
    /// Set when this connection authenticated with a session credential:
    /// the session label above is then *proven*, not merely asserted in a
    /// header. `null` means a plain agent token with a header label, which
    /// is still how every existing client connects.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session_identity: Option<SessionIdentity>,
    /// Channel this session posts to when `post_message` is called with
    /// neither `channel` nor `to` — the one named after your session, if the
    /// team has one. `null` means there is none, so you must always say where
    /// a message goes.
    pub default_channel: Option<String>,
    /// Number of unread direct messages waiting for you.
    pub unread_direct_messages: i64,
    /// Tasks currently claimed by you and not yet completed.
    pub open_claimed_tasks: i64,
}

// ------------------------------------------------------------------ agents --

#[derive(Debug, Serialize, JsonSchema)]
pub struct AgentInfo {
    pub name: String,
    pub display_name: Option<String>,
    /// Which working context the fields below describe — usually a repository
    /// name. Absent for the shared session, used by clients that send no
    /// `X-Crew-Session` header, so a roster of teammates who use no sessions
    /// serialises exactly as it did before sessions existed.
    ///
    /// Which row the summary describes, in order: a **live** session before a
    /// dead one, a **named** session before the shared one, then the most
    /// recently updated. Live comes first deliberately — a named session that
    /// died days ago should not outrank a shared row that is active now — so
    /// the shared row can win while every named session is offline. Read
    /// `sessions` when you need all of them; this is one of several.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session: Option<String>,
    /// One of `active`, `idle`, `offline`. `offline` means the presence lease
    /// expired, i.e. the agent has not sent a heartbeat recently.
    pub status: String,
    pub repo: Option<String>,
    pub branch: Option<String>,
    /// Free-text description of what this agent is currently doing.
    pub activity: Option<String>,
    /// Discovery labels the session set about itself (see `list_sessions`).
    /// Absent when never set.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub project: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub role: Option<String>,
    pub last_seen: Option<String>,
    /// True when *any* of this agent's sessions has a live presence lease.
    pub online: bool,
    /// Every working context this agent has open, most recently active first.
    /// Absent when there is only one — the fields above already describe it.
    /// A teammate with several entries here is working in several repositories
    /// at once, and each one claims tasks and holds locks independently.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub sessions: Vec<AgentSession>,
}

/// A session credential as the caller receives it. `session_token` is the
/// secret and appears exactly once, on registration.
#[derive(Debug, Serialize, JsonSchema)]
pub struct SessionCredential {
    /// The credential. Store it in a private file (0600) and send it as the
    /// bearer token from now on; it is not shown again. Absent on a renewal,
    /// which extends the credential you already hold.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session_token: Option<String>,
    pub session_id: String,
    /// The label this credential authenticates as.
    pub session: String,
    /// What a teammate puts in `to` to reach exactly this window.
    pub address: String,
    /// Connection epoch. Send it as `X-Crew-Epoch` to be fenced off cleanly
    /// if another process resumes this window after you.
    pub epoch: i64,
    pub expires_at: String,
    pub expires_in_seconds: i64,
}

/// What a session credential proves, reported by `whoami`.
#[derive(Debug, Serialize, JsonSchema)]
pub struct SessionIdentity {
    pub session_id: String,
    pub epoch: i64,
    pub registered_at: String,
    pub expires_at: String,
    pub expires_in_seconds: i64,
}

/// One working context of an agent: what that session is doing right now.
#[derive(Debug, Serialize, JsonSchema)]
pub struct AgentSession {
    /// Absent for the shared session.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session: Option<String>,
    pub status: String,
    pub repo: Option<String>,
    pub branch: Option<String>,
    pub activity: Option<String>,
    /// Discovery labels this session set about itself. Absent when never set.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub project: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub role: Option<String>,
    pub last_seen: Option<String>,
    pub online: bool,
}

/// One session as `list_sessions` reports it: addressable, with its
/// discovery labels. Two sessions may share every label and still be two
/// entries, because the address differs.
#[derive(Debug, Serialize, JsonSchema)]
pub struct SessionEntry {
    pub agent: String,
    /// Absent for the shared session.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session: Option<String>,
    /// What to put in `to` (or `ask_agent`'s `to`) to reach this session.
    /// `agent/session` for a named one; the bare `agent` for the shared
    /// session — and see `exact` before treating that as private.
    pub address: String,
    /// True when `address` reaches **this window and no other**. False for
    /// the shared session, whose address is the bare agent name: that is a
    /// broadcast to every window of that agent, named ones included, so it
    /// is the wrong place to send a private instruction. There is no address
    /// that reaches the shared session alone.
    pub exact: bool,
    pub project: Option<String>,
    pub role: Option<String>,
    pub repo: Option<String>,
    pub branch: Option<String>,
    pub activity: Option<String>,
    /// One of `active`, `idle`, `busy`, `blocked`, `offline`.
    pub status: String,
    pub online: bool,
    pub last_seen: Option<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct SessionList {
    pub sessions: Vec<SessionEntry>,
    /// Sessions returned. When it equals the limit there may be more: narrow
    /// with `project` or `role`.
    pub count: usize,
    pub limit: i64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct AgentList {
    pub agents: Vec<AgentInfo>,
    /// Agents with at least one live session — people, not sessions.
    pub online_count: usize,
}

// -------------------------------------------------------------- messaging --

#[derive(Debug, Serialize, JsonSchema)]
pub struct ChannelInfo {
    pub name: String,
    pub topic: Option<String>,
    pub message_count: i64,
    pub created_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ChannelList {
    pub channels: Vec<ChannelInfo>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct MessageInfo {
    pub id: i64,
    pub from: String,
    /// Which of the sender's working contexts wrote this; `null` is their
    /// shared session. Reply to `from/from_session` to reach the window that
    /// is waiting, rather than whichever one notices first.
    pub from_session: Option<String>,
    /// True when this was posted as an announcement: something the sender
    /// judged worth interrupting the whole team for, so it reaches every
    /// session regardless of which channel they are focused on.
    pub announce: bool,
    /// Channel name for channel messages; `null` for direct messages.
    pub channel: Option<String>,
    /// Recipient handle for direct messages; `null` for channel messages.
    pub to: Option<String>,
    /// Set when this direct message was addressed to one working context of
    /// the recipient rather than to the person. `null` means every session of
    /// theirs sees it.
    pub to_session: Option<String>,
    pub body: String,
    pub reply_to: Option<i64>,
    #[schemars(schema_with = "any_json_schema")]
    pub metadata: serde_json::Value,
    /// Files attached to this message; fetch content with get_attachment.
    pub attachments: Vec<AttachmentMeta>,
    pub created_at: String,
}

#[derive(Debug, Serialize, serde::Deserialize, JsonSchema)]
pub struct AttachmentMeta {
    /// Pass this id to get_attachment to download the content.
    pub id: i64,
    pub filename: String,
    pub content_type: String,
    pub size_bytes: i64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct AttachmentContent {
    pub id: i64,
    pub filename: String,
    pub content_type: String,
    pub size_bytes: i64,
    pub uploaded_by: String,
    pub created_at: String,
    /// The file content, base64-encoded.
    pub data_base64: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct PostMessageResult {
    pub message: MessageInfo,
    /// Handles that can now see this message.
    pub delivered_to: Vec<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct MessageList {
    pub messages: Vec<MessageInfo>,
    /// The scope that was actually read, after normalisation.
    pub scope: String,
    /// Read cursor position after this call. Messages at or below this id will
    /// not be returned again when `only_new` is true.
    pub cursor: i64,
    /// True when the result hit `limit` and older/newer messages remain.
    pub truncated: bool,
}

// ------------------------------------------------------------------ tasks --

#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskInfo {
    pub key: String,
    pub title: String,
    pub description: Option<String>,
    /// One of `open`, `claimed`, `done`, `cancelled`. A claim whose lease
    /// lapsed reads as `open`: anyone may take it, the former holder
    /// included (see `lapsed_holder`).
    pub status: String,
    /// Keys of tasks this one depends on.
    pub depends_on: Vec<String>,
    /// True while any dependency is not yet done/cancelled. Blocked tasks
    /// cannot be claimed.
    pub blocked: bool,
    pub claimed_by: Option<String>,
    /// Which of `claimed_by`'s working contexts holds the claim; `null` is
    /// their shared session. A claim belongs to a session, not to a person —
    /// your own other session cannot renew, release or steal this one.
    pub claimed_session: Option<String>,
    pub claimed_at: Option<String>,
    /// When the current claim expires. After this instant another agent may
    /// steal the task, so renew the lease if you are still working on it.
    pub lease_expires_at: Option<String>,
    /// Seconds left on the claim, so you can decide whether waiting is
    /// reasonable without doing the arithmetic.
    pub lease_seconds_remaining: Option<i64>,
    /// True when the last claim lapsed and nobody has claimed the task
    /// since: it is `open`, and `lapsed_holder` says who let it go.
    pub lease_expired: bool,
    /// Who held the claim that lapsed, while the task stays unclaimed. Not a
    /// holder: nobody has to be asked before claiming it.
    pub lapsed_holder: Option<String>,
    pub result: Option<String>,
    #[schemars(schema_with = "any_json_schema")]
    pub metadata: serde_json::Value,
    /// Files attached to this task; fetch content with get_attachment.
    pub attachments: Vec<AttachmentMeta>,
    pub created_by: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskList {
    pub tasks: Vec<TaskInfo>,
    pub open: i64,
    pub claimed: i64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ClaimResult {
    pub claimed: bool,
    pub task: Option<TaskInfo>,
    /// Present when `claimed` is false: why the claim did not succeed.
    pub reason: Option<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskEventInfo {
    pub event: String,
    pub agent: Option<String>,
    pub detail: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct TaskDetail {
    pub task: TaskInfo,
    pub history: Vec<TaskEventInfo>,
}

// ------------------------------------------------------------------ notes --

#[derive(Debug, Serialize, JsonSchema)]
pub struct NoteInfo {
    pub scope: String,
    pub key: String,
    pub value: String,
    pub tags: Vec<String>,
    pub updated_by: Option<String>,
    pub updated_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct NoteList {
    pub notes: Vec<NoteInfo>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct NoteRef {
    pub scope: String,
    pub key: String,
    pub found: bool,
    pub note: Option<NoteInfo>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct Ack {
    pub ok: bool,
    pub detail: String,
}

// ------------------------------------------------------------------ locks --

#[derive(Debug, Serialize, JsonSchema)]
pub struct LockInfo {
    pub name: String,
    pub holder: String,
    /// Which of the holder's working contexts took it; `null` is their shared
    /// session. A lock belongs to a session — your own other session cannot
    /// release it or take it over while it is live.
    pub holder_session: Option<String>,
    pub purpose: Option<String>,
    pub acquired_at: String,
    /// When the lock lapses on its own if not renewed.
    pub expires_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct LockList {
    pub locks: Vec<LockInfo>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct LockResult {
    pub acquired: bool,
    pub lock: Option<LockInfo>,
    /// Present when `acquired` is false: who holds it and until when.
    pub reason: Option<String>,
}

// ----------------------------------------------------------------- events --

#[derive(Debug, Serialize, JsonSchema)]
pub struct WaitEvent {
    /// One of `message`, `task`, `lock`, `note`.
    pub kind: String,
    /// Human-readable one-liner of what happened.
    pub summary: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct WaitResult {
    /// True when something happened; false when the timeout elapsed quietly.
    pub woke: bool,
    pub timed_out: bool,
    pub events: Vec<WaitEvent>,
    /// Unread direct messages after the wait — if > 0, call read_messages.
    pub unread_direct_messages: i64,
    /// What to do next, e.g. which tool to call to fetch the details.
    pub suggestion: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct AskResult {
    /// True when the teammate answered before the timeout.
    pub answered: bool,
    /// The agent the question was addressed to.
    pub to: String,
    /// Id of the question message. On timeout, pass it back as
    /// `resume_message_id` to keep waiting without re-sending the question.
    pub question_message_id: i64,
    /// The answer: their reply to the question, or failing that their first
    /// direct message to you after it.
    pub answer: Option<MessageInfo>,
    /// What to do next.
    pub suggestion: String,
}

// ----------------------------------------------------------------- digest --

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestMessage {
    pub from: String,
    pub body: String,
    pub at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestChannel {
    pub name: String,
    pub message_count: i64,
    pub last_messages: Vec<DigestMessage>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestTask {
    pub key: String,
    pub title: String,
    pub status: String,
    pub claimed_by: Option<String>,
    pub result: Option<String>,
    pub updated_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestNote {
    pub scope: String,
    pub key: String,
    pub updated_by: Option<String>,
    pub updated_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestAgent {
    pub name: String,
    pub activity: Option<String>,
    pub last_seen: Option<String>,
    pub online: bool,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct DigestResult {
    /// Window covered, in hours.
    pub hours: i64,
    pub channels: Vec<DigestChannel>,
    /// Tasks whose state changed inside the window, newest first.
    pub tasks_moved: Vec<DigestTask>,
    pub open_tasks: i64,
    pub claimed_tasks: i64,
    pub notes_updated: Vec<DigestNote>,
    pub agents_seen: Vec<DigestAgent>,
    pub active_locks: Vec<LockInfo>,
}

// ----------------------------------------------------------- conversations --

/// A project: the unit a conversation can be visible to. Access is an
/// explicit grant, never inferred from a directory or a role label.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ProjectInfo {
    pub id: String,
    pub name: String,
    /// Agents with an explicit grant. Only visible to someone who has one.
    pub members: Vec<String>,
    pub archived: bool,
    pub created_at: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ProjectList {
    pub projects: Vec<ProjectInfo>,
}

/// One conversation as a caller sees it.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationInfo {
    pub id: String,
    pub title: String,
    /// `project` (everyone with access to the project can read it) or
    /// `private` (only its members). Fixed at creation.
    pub visibility: String,
    /// Absent for a private conversation.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub project: Option<String>,
    pub created_by: String,
    pub created_at: String,
    pub archived: bool,
    /// Highest logical sequence in the thread. Messages are paged by this.
    pub last_seq: i64,
    /// Your own membership, when you have one.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub membership: Option<MembershipInfo>,
    /// Everyone in the thread. Only returned to a member.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub members: Vec<MembershipInfo>,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct MembershipInfo {
    pub membership_id: String,
    pub agent: String,
    /// Absent for the shared session.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session: Option<String>,
    /// `agent/session`, or the bare agent for the shared session.
    pub address: String,
    /// `owner`, `moderator`, `participant` or `observer`.
    pub role: String,
    /// `invited`, `active`, `left` or `removed`.
    pub state: String,
    /// Lowest sequence this member may read; `null` means from the start.
    pub history_from_seq: Option<i64>,
    pub invited_at: String,
    pub accepted_at: Option<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationList {
    pub conversations: Vec<ConversationInfo>,
}

/// What a send returns. `stored` and `publication` are facts about
/// persistence at the moment of this reply, never about anyone having read
/// anything.
#[derive(Debug, Serialize, JsonSchema)]
pub struct SentMessage {
    pub message_id: String,
    pub conversation_id: String,
    pub seq: i64,
    /// True when the backend that holds this body had confirmed it when this
    /// reply was written. On a thread stored in Postgres that is the send's
    /// own commit, so it is always true. On a thread published through an
    /// outbox (a team routed to JetStream) the send is accepted and recorded
    /// first and the backend's answer comes later, so a fresh send says
    /// `false` with `publication: "pending_publication"`. That state is not
    /// final: it settles as `stored`, or as `failed` if the backend refuses
    /// the body for good. While it is pending, do NOT send the message again.
    /// Watch it settle with
    /// `get_conversation_message` or `get_message_receipts` (`stored_at`),
    /// or repeat the call with the same `request_id`, which returns the same
    /// message with its current state.
    pub stored: bool,
    /// Where the body stands with its backend right now, in the same words
    /// a read of the message uses: `stored`, `pending_publication`
    /// (accepted, not yet confirmed) or `failed` (it will not be published;
    /// the message keeps its place and the gap stays visible). `stored` is
    /// true exactly when this is `"stored"`.
    pub publication: String,
    /// Who the message was addressed to, snapshotted now. A later join never
    /// enters this list.
    pub recipients: Vec<String>,
    pub created_at: String,
}

/// One message of a thread.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationMessage {
    pub message_id: String,
    pub seq: i64,
    pub from: String,
    /// `agent/session` of the sender, for an exact reply.
    pub from_address: String,
    /// The text the sender wrote, or an EMPTY STRING when `unavailable` is
    /// set. Check `unavailable` before quoting or summarising: an empty
    /// body with a reason there is a body this bus cannot give you — not
    /// yet, or not any more — never an empty message from your teammate.
    pub body: String,
    pub reply_to: Option<String>,
    pub metadata: serde_json::Value,
    pub created_at: String,
    /// Your own observations on this message, when you are a recipient.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub my_receipt: Option<ReceiptInfo>,
    /// Where this body stands with the backend that holds it: `stored`
    /// normally, and `pending_publication`, `failed` or `tombstoned` when
    /// the body is not here. A thread on the default Postgres backend is
    /// always `stored`.
    pub publication: String,
    /// Always present. `null` when `body` is the real text; otherwise why
    /// the body is not here, and `body` is an empty placeholder. The reason
    /// says which case it is: a backend that cannot be reached right now
    /// (try again later), a body that was never stored, or one the backend
    /// no longer holds (those two will not come back). The message keeps
    /// its place in the sequence, its sender and its receipts either way: a
    /// gap you can see and read about is not the same as a gap.
    #[serde(default)]
    pub unavailable: Option<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationRead {
    pub conversation_id: String,
    pub messages: Vec<ConversationMessage>,
    /// Pass as `after_seq` to continue. Absent when the thread is exhausted.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub next_after_seq: Option<i64>,
    /// Lowest sequence you may read in this thread.
    pub history_from_seq: Option<i64>,
}

/// One reference to a message, as a recipient's inbox hands it over. It
/// carries no body: the body is read separately, with a current access
/// check at that moment.
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct InboxReference {
    /// Pass this to `confirm_inbox_delivery` once you hold the reference
    /// durably. Until you do, nothing has been marked delivered.
    pub delivery_id: String,
    pub message_id: String,
    pub conversation_id: String,
    pub seq: i64,
    pub from: String,
    /// `agent/session` of the sender, for an exact reply.
    pub from_address: String,
    pub created_at: String,
    /// True when this reference has been offered before: an earlier
    /// confirmation was lost, or the process holding it went away. Handling
    /// it twice must change nothing.
    pub redelivered: bool,
    /// `broker` when it came from the durable inbox, `bus` when it was
    /// rebuilt from the bus's own records after an expiry, a deleted
    /// consumer, or a team that is not routed to a broker at all.
    pub source: String,
    /// `message` — this message was addressed to you — or `receipt`: a
    /// message *you sent* has a receipt worth reading again. A receipt
    /// reference never means someone read anything; `get_message_receipts`
    /// says what actually happened.
    pub kind: String,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct InboxBatch {
    pub references: Vec<InboxReference>,
    /// How many of them the broker supplied. The rest were rebuilt from the
    /// bus's own records, which are the authority.
    pub from_broker: i64,
    /// True when the batch filled: call again.
    pub more: bool,
    /// Present when something is worth saying about where these references
    /// came from: an unreachable broker, a missing consumer, a team whose
    /// conversations live on Postgres.
    ///
    /// Written for you, not for an operator. It never carries backend
    /// detail — no stream names, no error codes, no credentials — and never
    /// asks you to run something only an operator can; that detail is in
    /// the server log. A note is not an error and not a reason to retry the
    /// call that produced it: the page beside it is complete either way.
    /// It never decides pagination: when it mentions paging it only repeats
    /// that `more` is the authority, and `more` alone says whether to call
    /// again.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub note: Option<String>,
}

/// What one window's inbox holds. The two sides are reported separately on
/// purpose: they answer different questions, and averaging them would hide
/// exactly the case worth seeing.
#[derive(Debug, Serialize, JsonSchema)]
pub struct InboxState {
    pub address: String,
    /// Messages addressed to this window that it has never confirmed
    /// holding. The authoritative number.
    pub undelivered: i64,
    /// References handed to a process that has not confirmed them. A
    /// non-zero number here after a crash is expected: they are offered
    /// again.
    pub handed_out_unconfirmed: i64,
    /// What the broker still holds for this window, when there is one.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub broker_pending: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub broker_awaiting_ack: Option<i64>,
    /// False means the durable consumer is gone (expired, or removed).
    /// That is not an empty inbox: the bus's own records still have it.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub broker_consumer_present: Option<bool>,
}

/// Five independent observations. An absent timestamp means *not observed*,
/// never "assumed": a cursor moving is not a person reading, and a host that
/// cannot confirm injection leaves `presented_at` null.
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct ReceiptInfo {
    pub agent: String,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub session: Option<String>,
    pub address: String,
    pub stored_at: Option<String>,
    pub delivered_at: Option<String>,
    /// Null when the host cannot confirm the message reached the model. That
    /// is unknown, not "no".
    pub presented_at: Option<String>,
    pub acknowledged_at: Option<String>,
    /// The recipient said it acted on this. It does not complete a task or
    /// merge anything by itself.
    pub resolved_at: Option<String>,
    pub note: Option<String>,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct MessageReceipts {
    pub message_id: String,
    pub seq: i64,
    /// One entry per recipient at acceptance time.
    pub receipts: Vec<ReceiptInfo>,
    pub acknowledged: usize,
    pub resolved: usize,
    pub total: usize,
}

/// What `wait_for_conversation_updates` reports.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationUpdates {
    pub conversations: Vec<ConversationActivity>,
    pub waited_seconds: i64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct ConversationActivity {
    pub conversation_id: String,
    pub title: String,
    /// Highest sequence currently stored in the thread. It is not a read
    /// cursor: it does not move with what you have read or acknowledged.
    pub last_seq: i64,
    /// Messages addressed to you that you have not acknowledged.
    pub unacknowledged: i64,
}

/// Result of a membership transfer proposal or acceptance.
#[derive(Debug, Serialize, JsonSchema)]
pub struct TransferResult {
    pub conversation_id: String,
    /// The membership that will be superseded once the target accepts.
    pub from_address: String,
    pub to_address: String,
    pub state: String,
}