M3: channels/threads + cron delivery + search + outbox sync
Gateway plugin: - ChannelDirectory (SQLite): channels + threads, friendly-name resolution - FTS5 search bridge over state.db (scope all/chat, LIKE fallback) - Outbox (monotonic cursor, 72h retention) for reconnect catch-up - adapter: channel.* handlers, search, sync, cron target parsing - ws_server: M3 frame routing + hello.ack sync cursor App (KMP): - ChannelStore (directory cache) + lane-aware ChatStore (per chat/thread) - GatewayClient.sendFrame; IrisController channel/search/nav actions - ChatScreen: channel drawer, thread toggle, topic switcher, search overlay
This commit is contained in:
1 parent
218c50d688
commit
9b511fdd28
13 files changed
+2035
-259
No files matched your search
@@ -17,7 +17,7 @@ Milestone M5: notification, fcm.register, read.receipt.
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
@@ -50,6 +50,24 @@ TYPE_TOOL_END = "tool.end"
|
||||
# Intermediate assistant beat (M2)
|
||||
TYPE_COMMENTARY = "commentary"
|
||||
|
||||
# Channels / threads (M3)
|
||||
TYPE_CHANNEL_CREATE = "channel.create"
|
||||
TYPE_CHANNEL_RENAME = "channel.rename"
|
||||
TYPE_CHANNEL_SET_DEFAULT = "channel.set_default"
|
||||
TYPE_CHANNEL_DELETE = "channel.delete"
|
||||
TYPE_CHANNEL_CREATED = "channel.created"
|
||||
TYPE_CHANNEL_RENAMED = "channel.renamed"
|
||||
TYPE_CHANNEL_DELETED = "channel.deleted"
|
||||
TYPE_CHANNEL_LIST = "channel.list"
|
||||
|
||||
# Search (M3)
|
||||
TYPE_SEARCH = "search"
|
||||
TYPE_SEARCH_RESULTS = "search.results"
|
||||
|
||||
# Reconnect catch-up (M3 outbox; extended by M5 push)
|
||||
TYPE_SYNC = "sync"
|
||||
TYPE_SYNC_DONE = "sync.done"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error codes (``error`` frame payload.code)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -352,6 +370,80 @@ def commentary(
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Channel directory frames (M3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _channel_payload(entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Project a directory entry onto the wire shape."""
|
||||
payload: Dict[str, Any] = {
|
||||
"chat_id": entry.get("chat_id"),
|
||||
"name": entry.get("name"),
|
||||
"kind": entry.get("kind", "channel"),
|
||||
}
|
||||
if entry.get("parent_chat_id") is not None:
|
||||
payload["parent_chat_id"] = entry["parent_chat_id"]
|
||||
if entry.get("is_default"):
|
||||
payload["is_default"] = True
|
||||
if entry.get("archived"):
|
||||
payload["archived"] = True
|
||||
return payload
|
||||
|
||||
|
||||
def channel_created(entry: Dict[str, Any]) -> Frame:
|
||||
"""Broadcast: a channel/thread was created."""
|
||||
return Frame(type=TYPE_CHANNEL_CREATED, payload=_channel_payload(entry))
|
||||
|
||||
|
||||
def channel_renamed(entry: Dict[str, Any]) -> Frame:
|
||||
"""Broadcast: a channel/thread was renamed."""
|
||||
return Frame(type=TYPE_CHANNEL_RENAMED, payload=_channel_payload(entry))
|
||||
|
||||
|
||||
def channel_deleted(chat_id: str) -> Frame:
|
||||
"""Broadcast: a channel was archived (soft-deleted)."""
|
||||
return Frame(type=TYPE_CHANNEL_DELETED, payload={"chat_id": chat_id})
|
||||
|
||||
|
||||
def channel_list(channels: List[Dict[str, Any]]) -> Frame:
|
||||
"""Full directory (response to a ``channel.list`` request)."""
|
||||
return Frame(
|
||||
type=TYPE_CHANNEL_LIST,
|
||||
payload={"channels": [_channel_payload(c) for c in channels]},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Search frames (M3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def search_results(
|
||||
query: str,
|
||||
scope: str,
|
||||
hits: List[Dict[str, Any]],
|
||||
*,
|
||||
id: Optional[int] = None,
|
||||
) -> Frame:
|
||||
"""Response to a ``search`` request.
|
||||
|
||||
Each hit: ``{message_id, chat_id, thread_id, role, snippet, ts}``.
|
||||
"""
|
||||
return Frame(
|
||||
type=TYPE_SEARCH_RESULTS,
|
||||
id=id,
|
||||
payload={"query": query, "scope": scope, "hits": hits},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sync frames (M3 outbox)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def sync_done(cursor: int, *, id: Optional[int] = None) -> Frame:
|
||||
"""Terminal frame of a ``sync`` replay: the new cursor to persist."""
|
||||
return Frame(type=TYPE_SYNC_DONE, id=id, payload={"cursor": cursor})
|
||||
|
||||
|
||||
def error(code: str, message: str, *, id: Optional[int] = None) -> Frame:
|
||||
return Frame(type=TYPE_ERROR, id=id, payload={"code": code, "message": message})
|
||||
|
||||
|
||||
Reference in new issue
Block a user