Files
2026-07-13 13:12:33 +08:00

18 KiB

Session View Contract

This document is the shared contract for the Web UI session optimization work on feature/session-contract-ui-backend.

The goal of this branch is to improve Web UI session discovery, grouping, labeling, and readability. Backend changes exist to support that UI work with a stable session view contract. This is not a backend-only architecture rewrite.

Problem

The current Web UI often treats the raw session key as both an address and a semantic data source. That forces frontend code to infer concepts from strings such as agent:*:webchat:*, :cli:, :subagent:, :cron:, or :thread:.

That is fragile because a session key may encode several independent concepts:

  • routing address
  • agent/workspace ownership
  • entry surface
  • external channel identity
  • direct/group/channel conversation topology
  • thread/topic modifiers
  • subagent/task origin
  • cron origin or delivery
  • legacy compatibility shape

The UI should not own those interpretations. The backend must provide a UI-ready session view, and the frontend should render from that view.

Product Model

The Web UI has two different surfaces that must not collapse into one another:

  • Conversations: the daily navigation surface for items a user wants to open, read, and continue from a user-centered perspective.
  • Sessions: the lower-level ledger/debug surface for all persisted runtime records, including WebChat, CLI, channel threads, cron runs, subagents, system tasks, deletion, filtering, and raw key inspection.

Conversations is not a smaller Sessions table. It should group items by user entry point:

  • Chats: sessionKind: "chat"
  • Channels: sessionKind: "channel"
  • Automations: sessionKind: "cron"

Task and system sessions should normally stay in the Sessions ledger unless a dedicated background-work UI explicitly opts into them.

New chat is only a WebChat creation flow:

New chat -> choose agent -> create/open WebChat

It must not create cron jobs, channel sessions, subagent tasks, or system/task sessions. Cron and channel creation/configuration belong to their own Automations/Channels surfaces.

API

Preferred RPC:

rpc.call("sessions.list", { limit: 200, view: "session-list-v1" })

If a REST endpoint is used, /api/sessions should support equivalent limit and view parameters before the chat session selector relies on it for a larger list. The current default session list size may be too small for selector UI.

The response shape remains backward compatible:

interface SessionsListResponseV1 {
  sessions: SessionListItemV1[];
  count: number;
  ts: number;
}

Existing callers that send only { limit } must continue to work.

Contract Fields

interface SessionListItemV1 {
  key: string;
  sessionId?: string;

  // Legacy/stored agent id. Kept for compatibility with existing callers.
  agentId?: string;

  // Effective routing/workspace owner. New UI should prefer this.
  effectiveAgentId: string;

  sessionKind: "chat" | "channel" | "task" | "cron" | "system" | "unknown";

  surface:
    | "webchat"
    | "cli"
    | "tui"
    | "mcp"
    | "slack"
    | "feishu"
    | "wecom"
    | "telegram"
    | "discord"
    | "dingtalk"
    | "matrix"
    | "qq"
    | "cron"
    | "subagent"
    | "unknown";

  conversationKind: "main" | "direct" | "group" | "channel" | "unknown";

  thread?: {
    id: string;
    kind: "thread" | "topic";
  } | null;

  title: string;
  subtitle?: string;
  groupLabel: string;

  updatedAt: number;
  messageCount: number;
  status: string;

  runStatus:
    | "idle"
    | "queued"
    | "running"
    | "interrupted"
    | "failed"
    | "timeout"
    | "cancelled";

  // Whether the current Web UI should enable its standard chat composer.
  interactive: boolean;

  channelContext?: {
    name?: string;
    id?: string;
    accountId?: string;
    peerId?: string;
    threadId?: string;
  };

  parent?: {
    key: string;
    taskId?: string;
    spawnDepth?: number;
  } | null;

  cron?: {
    jobId?: string;
    sessionTarget?: "main" | "isolated" | "current" | "session";
    originSessionKey?: string;
    targetSessionKey?: string;
  } | null;
}

Field Semantics

key

Public session routing address. The UI may use it for opening, resuming, copying, deleting, and debugging a session. The UI must not parse it for semantic classification.

sessionId

Backend transcript/storage identity when available. It is not the primary UI routing address.

agentId

Legacy or stored agent id. This remains available for compatibility with existing CLI/TUI/MCP and older UI paths.

effectiveAgentId

The agent id that should be used for UI ownership, workspace/routing display, and agent badges. This handles legacy rows where the stored agentId may be main while routing should follow another agent.

sessionKind

The lifecycle bucket of the session:

  • chat: human-facing interactive chat session, including WebChat, CLI, TUI, MCP, and main-agent chat sessions.
  • channel: external platform conversation session.
  • task: runtime/background task session, especially subagent work.
  • cron: cron-owned isolated run session.
  • system: internal/system session when the backend exposes one.
  • unknown: backend cannot classify the row.

This field is the primary grouping input for the Conversations surface. If a design sketch calls this concept conversationKind: "chat" | "channel" | "cron" | "task", the contract name for that concept is sessionKind.

surface

The entry surface or platform that produced the session view. Examples: webchat, cli, tui, mcp, feishu, slack, telegram, cron, subagent.

Current terminal TUI sessions are CLI-compatible. They should normally report surface: "cli" because the existing TUI gateway path creates CLI sessions and uses the CLI gateway client contract. Use surface: "tui" only if a future TUI path explicitly marks sessions as TUI-owned.

Known public channel surfaces align with the channel adapter contract: slack, discord, feishu, dingtalk, wecom, qq, matrix, and telegram. Unknown or not-yet-public adapters should degrade to surface: "unknown" while preserving display metadata in channelContext.

conversationKind

The conversation topology:

  • main: an agent's main/private session.
  • direct: direct/private one-to-one conversation.
  • group: group, room, or multi-person conversation.
  • channel: platform channel/broadcast-style conversation.
  • unknown: backend cannot classify the topology.

Thread and topic information must be expressed through thread, not by adding conversationKind: "thread".

thread

Optional modifier for platform thread/topic context. It does not replace conversationKind.

title

Primary user-facing label. UI should prefer this over the raw key.

subtitle

Secondary context such as source, agent, channel, parent, or recent context.

groupLabel

UI grouping label. Session selector and Sessions page grouping should use this field instead of deriving groups from the key.

updatedAt

Epoch milliseconds for recency sorting and relative time display.

messageCount

Transcript/message count for display.

status

Persisted session lifecycle status. Current backend values include running, done, failed, killed, and timeout. Frontend should treat this as a backend lifecycle string and use runStatus for idle/running turn badges.

runStatus

Runtime task status for current/last turn display. This is separate from the persisted session lifecycle status.

interactive

Whether the current Web UI should enable its standard chat composer for this row. This is not the same as visibility. A non-interactive row may still appear in Conversations or Sessions as a readable item.

Default rules:

  • WebChat rows should be interactive.
  • CLI/TUI/MCP rows are compatible ledger rows; do not enable the Web UI composer unless backend explicitly marks them interactive.
  • Channel rows are readable from the Web UI, but the standard WebChat composer should remain disabled unless a safe channel-reply flow exists.
  • Cron, subagent, task, and system rows should normally be non-interactive.

If the UI later needs more nuance, add a structured field such as openMode instead of inferring behavior from the key.

channelContext

Optional external channel identity and delivery metadata. This should be display-only in the UI unless a feature explicitly needs routing details.

Do not confuse this with the legacy channel field already present in sessions.list. The legacy field remains for CLI/TUI/older callers and may be a string. New UI should use surface and channelContext.

parent

Optional subagent/task parent relationship.

cron

Optional cron metadata. Cron metadata does not automatically make an existing webchat/channel session a cron session.

error_id

Optional short reference id on session.event.error payloads (and rendered into user-facing error text as (ref: <error_id>)). Joins the error to its durable turn_errors record for diagnostics. Additive; may be absent on older gateways or when no record was written.

Backend Rules

The backend owns session classification and display normalization.

Backend generation should prefer explicit structured data before legacy key fallbacks:

  1. Session row fields, including agent id, display name, channel fields, delivery context, parent session key, and origin metadata.
  2. Route/source metadata such as source kind, channel kind, channel id, thread id, and interaction mode.
  3. Task runtime rows for runStatus, subagent/task status, and parent relationships.
  4. Cron job/session metadata for cron-owned isolated runs and cron delivery context.
  5. Legacy key parsing only as a compatibility fallback inside backend normalization.

The frontend must not duplicate backend key parsing.

Backend compatibility requirements:

  • Keep sessions.list { limit } working.
  • Keep sessions.create -> { key, sessionId } working.
  • Keep sessions.resolve({ key }) working.
  • Keep chat.history({ sessionKey }) working.
  • Keep existing row fields such as agent_id, agentId, updated_at, updatedAt, message_count, entry_count, sourceKind, and channelKind.
  • Add new contract fields without deleting or changing the old shape.
  • Treat existing fields as compatibility output, not as the canonical semantic source for the new Web UI.
  • Keep the existing CLI and current terminal TUI gateway contracts thin: sessions.create({ kind: "cli" }), sessions.list({ limit }), sessions.resolve({ key }), and chat.history({ sessionKey }) must not gain required new parameters.

Frontend Rules

Frontend should render from contract fields:

  • Use sessionKind for the primary Conversations sections: Chats, Channels, and Automations.
  • Use groupLabel for second-level grouping within a section.
  • Use title as primary text.
  • Use subtitle as secondary text.
  • Use effectiveAgentId for agent badges and agent ownership display.
  • Use messageCount for message count.
  • Use updatedAt for relative time.
  • Use runStatus for runtime badges.
  • Use sessionKind, surface, and conversationKind for icons, colors, and high-level visual treatment.
  • Use interactive to decide whether to enable the standard Web UI composer.
  • Treat thread or topic as a modifier, not as a separate conversation kind.
  • Use key only for open/resume/copy/delete/RPC/debug actions.

If the UI needs a missing semantic field, backend should add it to this contract instead of the frontend deriving it from the key.

Sessions page rules:

  • Show all rows returned by sessions.list, including WebChat, CLI, channel, channel thread/topic, cron, subagent, task, system, and unknown rows.
  • Keep raw key visible or easily inspectable.
  • Favor filtering, deletion, status inspection, debugging, and resume/open actions over daily navigation grouping.

New chat rules:

  • Create only WebChat sessions.
  • Ask for or infer the target agent, then call the WebChat creation/open flow.
  • Do not create cron, channel, subagent, task, or system rows.

Forbidden Frontend Behavior

Frontend must not:

  • parse session.key to determine session kind
  • parse session.key to determine agent ownership
  • group by key tokens such as :webchat:, :cli:, :subagent:, :cron:, :thread:, or :topic:
  • infer external channel type from key segments
  • make the raw key the dominant user-facing label except in explicit debug/copy contexts
  • render a normal webchat/channel session as cron only because cron delivered into it
  • use New chat as a generic creation entry for cron, channel, subagent, task, or system sessions

Temporary fallback logic should be isolated, clearly marked, and should not become the primary UI path.

Display Examples

WebChat

{
  "key": "agent:main:webchat:default",
  "sessionId": "0d2d6f3e-8a41-40de-a3d4-5f05a3c4557a",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "chat",
  "surface": "webchat",
  "conversationKind": "direct",
  "thread": null,
  "title": "Web chat",
  "subtitle": "main",
  "groupLabel": "Web chat",
  "updatedAt": 1760000000000,
  "messageCount": 42,
  "status": "done",
  "runStatus": "idle",
  "interactive": true,
  "parent": null,
  "cron": null
}

CLI

{
  "key": "agent:main:cli:a1b2c3d4",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "chat",
  "surface": "cli",
  "conversationKind": "main",
  "title": "CLI session",
  "subtitle": "main",
  "groupLabel": "CLI",
  "updatedAt": 1760000000000,
  "messageCount": 12,
  "status": "done",
  "runStatus": "idle",
  "interactive": false
}

Subagent Task

{
  "key": "agent:main:subagent:760b927a",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "task",
  "surface": "subagent",
  "conversationKind": "unknown",
  "title": "Subagent task",
  "subtitle": "Spawned from Web chat",
  "groupLabel": "Subagents",
  "updatedAt": 1760000000000,
  "messageCount": 8,
  "status": "running",
  "runStatus": "running",
  "interactive": false,
  "parent": {
    "key": "agent:main:webchat:default",
    "taskId": "task-123",
    "spawnDepth": 1
  },
  "cron": null
}

Cron-Owned Isolated Run

{
  "key": "cron:daily-summary:run:abc123",
  "effectiveAgentId": "main",
  "sessionKind": "cron",
  "surface": "cron",
  "conversationKind": "unknown",
  "title": "Daily summary",
  "subtitle": "Cron isolated run",
  "groupLabel": "Cron",
  "updatedAt": 1760000000000,
  "messageCount": 4,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "cron": {
    "jobId": "daily-summary",
    "sessionTarget": "isolated"
  }
}

Cron Delivery Into Existing Channel Session

Cron delivery metadata may be present, but the existing channel session keeps its original visual identity.

{
  "key": "agent:main:feishu:group:oc_123",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "channel",
  "surface": "feishu",
  "conversationKind": "group",
  "title": "Launch room",
  "subtitle": "Feishu group",
  "groupLabel": "Feishu",
  "updatedAt": 1760000000000,
  "messageCount": 31,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "channelContext": {
    "name": "feishu",
    "id": "oc_123"
  },
  "cron": {
    "jobId": "launch-check",
    "sessionTarget": "session",
    "targetSessionKey": "agent:main:feishu:group:oc_123"
  }
}

External Channel Thread

{
  "key": "agent:main:slack:group:C123:thread:1717000000.000100",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "channel",
  "surface": "slack",
  "conversationKind": "group",
  "thread": {
    "id": "1717000000.000100",
    "kind": "thread"
  },
  "title": "C123 thread",
  "subtitle": "Slack thread",
  "groupLabel": "Slack",
  "updatedAt": 1760000000000,
  "messageCount": 16,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "channelContext": {
    "name": "slack",
    "id": "C123",
    "threadId": "1717000000.000100"
  }
}

Legacy Agent Mismatch

If stored agentId is main but the effective routing/workspace owner is another agent, the UI must show effectiveAgentId.

{
  "key": "agent:kid-project:webchat:test",
  "agentId": "main",
  "effectiveAgentId": "kid-project",
  "sessionKind": "chat",
  "surface": "webchat",
  "conversationKind": "direct",
  "title": "Kid project",
  "subtitle": "Web chat",
  "groupLabel": "Web chat",
  "updatedAt": 1760000000000,
  "messageCount": 5,
  "status": "done",
  "runStatus": "idle",
  "interactive": true
}

Backend Test Expectations

Backend contract tests should cover at least:

  • WebChat session
  • CLI session
  • subagent task session
  • cron-owned isolated session
  • cron delivery into an existing session
  • external channel session, such as Feishu, Slack, or Telegram
  • thread/topic modifier
  • legacy row where stored agentId differs from effectiveAgentId
  • unknown/fallback row that still produces usable title, groupLabel, and runStatus
  • current terminal TUI rows remaining CLI-compatible unless a future TUI path explicitly marks them as TUI-owned
  • interactive defaults for WebChat, CLI/TUI, channel, cron, subagent, task, and system rows

Frontend Acceptance Criteria

  • Conversations sidebar groups by sessionKind, then groupLabel.
  • Chat session selector displays title, subtitle, groupLabel, effectiveAgentId, and interactive behavior from the contract.
  • Sessions page displays title, subtitle, effectiveAgentId, messageCount, updatedAt, runStatus, and raw key/debug affordances.
  • No new frontend logic parses session.key for semantic classification.
  • Existing open/resume/copy/delete behavior still uses key.
  • WebChat, CLI, subagent, cron, and external channel sessions render from contract fields.
  • New chat creates only WebChat sessions.
  • Unknown or missing values degrade gracefully.
  • Fallback logic does not reintroduce key parsing as the primary path.