Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/feature-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ this area is reached from inside it unless stated otherwise.
| Terminal panel | Two shells on the gateway host: an app-wide docked PTY, and a per-chat terminal whose tab lives in that chat's panel state (opens on the chat's working dir; switches with the session) | Header terminal toggle (docked); chat right panel → **+** menu → **Terminal** (per-chat) | `components/BottomTerminalPanel.tsx`, `pages/chat/SidePanel.tsx` | `handlers/terminal.py` | `POST /api/terminal/sessions`, `GET /api/ws/terminal/{session_id}` |
| Browser panel | Live in-panel browser the agent drives | Right panel → **Browser** | `components/WebPreviewPanel.tsx` | `handlers/messaging.py` | `GET,POST /api/browser/view`, `POST /api/browser/command`, `POST /api/browser/command-result` |
| Notifications | Bell feed of agent-pushed notifications | Topbar bell → `/notifications` | `pages/NotificationsPage.tsx` | `handlers/messaging.py`, `handlers/notifications_push.py` | `GET /api/notifications`, `POST /api/notifications/ack`, `POST /api/notifications/push` |
| Crew Members | One durable pinned DM thread per crew member; the detail drawer lists the worker sessions the member is driving (live `slots` frames filtered on `created_by`) | `/members` — rail row when the crew preview is on | `pages/members/MembersPage.tsx` | `handlers/members.py`, `slot_projection.py` (`created_by`) | `GET /api/members`, `POST /api/members/{slug}/thread`, `GET /api/members/{slug}/activity`, `GET,PUT /api/members/{slug}/rules`, `GET /api/ws` (`slots`) |
| Crew Members | One durable pinned DM thread per crew member; the detail drawer lists the worker sessions the member is driving (live `slots` frames filtered on `created_by`) | `/members` — rail row when the crew preview is on | `pages/members/MembersPage.tsx` | `handlers/members.py`, `slot_projection.py` (`created_by`), `session_control.py` (`escalate_to_user`, the human as a peer), `src/kiro_crew/crew_conversation.py` (the per-member conversation index) | `GET /api/members`, `POST /api/members/{slug}/thread`, `GET /api/members/{slug}/activity`, `GET,PUT /api/members/{slug}/rules`, `GET /api/members/{slug}/conversation`, `GET /api/ws` (`slots`), `POST /api/session-control/escalate` |
| Channels | Group rooms with several agents in one thread | `/channels` (builtin app surface) | `pages/ChannelPage.tsx` | `handlers_channel.py` | `GET,POST /api/channels`, `POST /api/channels/{id}/messages`, `POST /api/channels/{id}/agents` |

The Notifications surface is registered `hiddenFromNav`: its route and badge
Expand Down
1 change: 1 addition & 0 deletions docs/system-specs/modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ agent loads only the one it needs.
| [file-search.md](file-search.md) | The `@`-mention file/folder search: index, ranking, `kinds` filter, and the sensitive-path symmetry. |
| [session-storage.md](session-storage.md) | What sessions cost on disk, and the user-initiated trash that reclaims it. |
| [session-control.md](session-control.md) | One chat session opening, stopping, and reading another. |
| [crew-conversation.md](crew-conversation.md) | The thin per-(human × member) conversation index: pointers, escalation lifecycle, derived `needs_you`. |
| [config.md](config.md) | The config schema, defaults, loading, and live reload. |
| [cli.md](cli.md) | Every CLI command, the gateway flags, and the test harness. |
| [heartbeat.md](heartbeat.md) | The liveness heartbeat and its restricted tool allowlist. |
Expand Down
113 changes: 113 additions & 0 deletions docs/system-specs/modules/crew-conversation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Crew Conversation Module

## Overview

A crew member's DM thread on the Crew Members page is a **conversation**
between one human and one member. Its lifetime is longer than any one session:
the pinned DM slot can be rebuilt or re-bound, and a worker session the member
dispatched may hand a result back into it. The conversation therefore has an
identity of its own — but it is deliberately **not** a second transcript.

`kiro_crew.crew_conversation` keeps that identity thin. The index stores
**pointers and lifecycle**, never bodies:

| Entry type | Shape | Meaning |
|------------|-------|---------|
| `escalation` | `{id, session_key, mid, from_session, state, created_ts, deadline, default_action, goal, options, answered_ts}` | One `session_escalate` delivery and where it stands. The text lives on the transcript row the pointer names. |

`state` ∈ `pending | answered | expired | defaulted | retracted`. The record also carries
`participants` (`{kind: human}` + `{kind: member, slug, name}`) and `sessions`
(every session key the conversation spans). The key is `dm:<slug>` today; a
later multi-member `goal:<id>` conversation is a new key shape and a longer
`participants` list, not a schema migration — which is why the record is shaped
as lists rather than a `member` / `session` pair.

## Storage

`$KIROCREW_HOME/members/<slug>/conversation.json`, beside `activity.jsonl`,
written whole with `atomic_write` under a per-slug lock (every writer is a
thread of the one gateway process; the lock is what keeps two concurrent
read-modify-writes from dropping each other's entry). It is small: entries
are capped at 500 (~100 KiB), evicting the oldest *settled* entries first — a
pending escalation is never dropped by the cap. It is **not** the trust binding (`trust/member-bindings/<slug>.json`):
the binding is the identity authority, strict-shape and keystone-gated; this is
mutable UI state and must stay out of that subtree. An unreadable or missing
file reads as an empty scaffold — the index is derived state and never fails a
roster, a slots frame, or an append.

`read_conversation` always parses (callers mutate what they get back, so no
shared record is ever cached). The hot path is different: `needs_you` runs
inside the slot projection on every sidebar push for every member slot, **on the
event loop**, so it reads an **in-memory** pending view (`_PENDING_CACHE`: the
pending records' ids and deadlines) and never touches the filesystem — not even
a `stat`. The view is primed off-loop once per member (`prime`, under the same
per-slug lock the writers hold) and refreshed by every writer after its own
write; a member that was never primed reads as nothing pending until a writer or
a prime touches it. The slug → cache-key mapping is bound to the raw
`KIROCREW_HOME` it was resolved under, so a data-home change reads as unprimed
rather than as another home's view.

## Derived state, not stored state

`needs_you` is **derived** from the pending escalation records at read time,
projected on the member slot's `slots` frame (`slot_projection.to_dict`) and on
the `GET /api/members` roster row (`needs_you`, `pending_escalations`). It is
never written to the slot: the slot is a process; the conversation is the thing
the human is in.

Lifecycle transitions:

- **pending → answered** — a *live* `user` row appended to the member DM slot
(`_ChatSlot.append`, `role == "user"`, `broadcast=True`, `mode == "member"`).
Which record it answers is one rule, shared with the chat projection that
draws the card: a row carrying `meta.escalation_id` (an option chip) answers
exactly that record; a row without one (typed text) answers the pending
record only when exactly one is pending — with none or several it answers
nothing, so an unrelated message cannot retire N open decisions. A replayed
row (`broadcast=False`: transcript rotation, fork, transfer) answers nothing.
A thread with nothing to change costs no write on an ordinary turn.
- **pending → defaulted / expired** — a passed `deadline` is applied lazily on
every read (`sweep_deadlines`): `defaulted` when a `default_action` was
declared (the member proceeds on it), `expired` otherwise. The file is only
rewritten the next time something else writes it (an answer, a new record),
so a deadline passing while the gateway is down still reads correctly on
restart.
- **pending → retracted / answered (recovery)** — the transcript is the truth
and the index a projection of it, so on restore the projection is re-derived
from the transcript (`reconcile_with_transcript`, run by the roster read
until nothing is deferred): a pending record whose card row the transcript
does not hold and that is older than `ORPHAN_GRACE_SECS` (120 s) is an
orphan — the gateway exited between the index write and the slot's flush —
and moves to `retracted` with `retracted_reason: orphan`; a pending record
whose card row is present and that a later durable `user` row with
`meta.human_reply` answers under the live rule moves to `answered` — the
gateway exited between the reply's save and the live hook's index write. A
record still inside the grace is deferred and reconsidered on the next read.
A record whose append failed outright is removed on the spot instead
(`retract_escalation`).

There is no background poller: nothing needs to *fire* at the deadline, because
the member that set it is the one that acts on it (it stated the default), and
the human-facing card counts down client-side from `meta.deadline`.

## Read surface

`GET /api/members/{slug}/conversation` (owner-only, like the thread endpoint;
app tokens refused) returns `public_view(record)`: the swept index plus
`needs_you` / `pending_escalations`. The chat projection on the Crew Members
page does not depend on it — it derives card state from the DM slot's own rows
(a later `user` row = answered; `now > deadline` = expired) so the view can never
disagree with the transcript it is rendered from.

## What is deliberately not here

- **No bodies.** A conversation never stores message text; a body lives in
exactly one place, the session JSONL, and is reached through `(session_key,
mid)`. `mid` is minted once (`history.mint_row_mid`) and survives restore,
which is what makes the pointer stable.
- **No per-message refs for the DM slot.** The DM session *is* the
conversation's main body; indexing every row of it would duplicate the
transcript's own ordering for nothing.
- **No approval records.** An escalation is a decision the member may take on
its own if unanswered; approvals (which block) are a different object and
are not modelled here.
Loading
Loading