feat(members): thin per-member conversation index - #8612
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound thin-index design, but the shipped spec documents a different cache mechanism than the code implements, hiding a restart-correctness coupling on a future PR. Watch
Suggestions
[DESIGN-REVIEWED] 7318305 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsFINDING -- src/kiro_crew/crew_conversation.py:350 -- distinct False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. Final review follows. First-Principles-Verdict: CONCERNS Honest, thin design — but the spec ships describing a stat-based cache and an HTTP endpoint this diff does not contain. What this change shipsIntent: give a member's DM thread an identity that outlives any single session, so escalations and worker hand-backs have somewhere to live — an ADDITION, declared as stacked groundwork with no caller yet.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7318305 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
c946e29 to
f81f043
Compare
3be88bd to
0675595
Compare
0675595 to
1d1628e
Compare
f81f043 to
50c9b25
Compare
1d1628e to
032d9e7
Compare
|
A crew member's DM thread on the Crew Members page is a conversation between one human and one member whose lifetime is longer than any single session: the DM slot can be rebuilt or re-bound, and a worker session the member dispatched may hand a result back into it. Give that conversation an identity — without making it a second transcript. `kiro_crew.crew_conversation` keeps a thin index at `$KIROCREW_HOME/members/<slug>/conversation.json`, beside `activity.jsonl` and deliberately NOT in the keystone-gated trust binding: * entries are pointers, never bodies — a `(session_key, mid)` ref into a session's JSONL, or a native escalation record whose text still lives on the transcript row (`mid` is minted once and survives restore, which is what makes the pointer stable); * `needs_you` is DERIVED from pending escalation records on read; a passed deadline reads as `defaulted` (a default action was declared) or `expired` without a write, and the human's reply marks every pending record `answered`; * the key is `dm:<slug>` today, but the record carries `participants` and `sessions` lists so a later multi-member `goal:<id>` conversation is a new key shape, not a schema migration. Reads are cached per slug on (mtime, size) because the slot projection will ask on every sidebar push. Deadline parsing accepts ISO-8601 or a duration (30m / 2h / 1d), bounded to 1 minute .. 7 days. Spec: docs/system-specs/modules/crew-conversation.md (+ README index row). No caller yet — the escalation PR on top of this one writes and reads it.
0921723 to
f76fe5f
Compare
f490176 to
7318305
Compare
A crew member's DM thread on the Crew Members page is a conversation between one human and one member whose lifetime is longer than any single session: the DM slot can be rebuilt or re-bound, and a worker session the member dispatched may hand a result back into it. Give that conversation an identity — without making it a second transcript. `kiro_crew.crew_conversation` keeps a thin index at `$KIROCREW_HOME/members/<slug>/conversation.json`, beside `activity.jsonl` and deliberately NOT in the keystone-gated trust binding: * entries are pointers, never bodies — a `(session_key, mid)` ref into a session's JSONL, or a native escalation record whose text still lives on the transcript row (`mid` is minted once and survives restore, which is what makes the pointer stable); * `needs_you` is DERIVED from pending escalation records on read; a passed deadline reads as `defaulted` (a default action was declared) or `expired` without a write, and the human's reply marks every pending record `answered`; * the key is `dm:<slug>` today, but the record carries `participants` and `sessions` lists so a later multi-member `goal:<id>` conversation is a new key shape, not a schema migration. Reads are cached per slug on (mtime, size) because the slot projection will ask on every sidebar push. Deadline parsing accepts ISO-8601 or a duration (30m / 2h / 1d), bounded to 1 minute .. 7 days. Spec: docs/system-specs/modules/crew-conversation.md (+ README index row). No caller yet — the escalation PR on top of this one writes and reads it.
A crew member's DM thread on the Crew Members page is a conversation between one human and one member whose lifetime is longer than any single session: the DM slot can be rebuilt or re-bound, and a worker session the member dispatched may hand a result back into it. Give that conversation an identity — without making it a second transcript. `kiro_crew.crew_conversation` keeps a thin index at `$KIROCREW_HOME/members/<slug>/conversation.json`, beside `activity.jsonl` and deliberately NOT in the keystone-gated trust binding: * entries are pointers, never bodies — a `(session_key, mid)` ref into a session's JSONL, or a native escalation record whose text still lives on the transcript row (`mid` is minted once and survives restore, which is what makes the pointer stable); * `needs_you` is DERIVED from pending escalation records on read; a passed deadline reads as `defaulted` (a default action was declared) or `expired` without a write, and the human's reply marks every pending record `answered`; * the key is `dm:<slug>` today, but the record carries `participants` and `sessions` lists so a later multi-member `goal:<id>` conversation is a new key shape, not a schema migration. Reads are cached per slug on (mtime, size) because the slot projection will ask on every sidebar push. Deadline parsing accepts ISO-8601 or a duration (30m / 2h / 1d), bounded to 1 minute .. 7 days. Spec: docs/system-specs/modules/crew-conversation.md (+ README index row). No caller yet — the escalation PR on top of this one writes and reads it.
The crew member DM session's kirocrew-dashboard MCP entry carried only the session key (and, since #8837, the bound port). The server resolves *which gateway* to call from its data home, so on any install where KIROCREW_HOME is set -- a pod, a second profile -- it presented the member's identity to the default home's gateway, which has no such member slot and refused every verb as caller_unidentified while tools/list looked healthy. Carry the same home override every managed Crew server already carries (_managed_mcp_env), through the same helper so the two cannot drift. On a default install the helper returns {} and the emitted entry is unchanged. The per-member conversation index (#8612) no longer rides in this PR; it travels with its consumer, #8613, which is stacked on this branch.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
…ate) A crew member that hits a wall -- a permission it lacks, something only a person can reach, a one-way door -- gets its own verb to raise it to the human: `session_escalate(message, deadline?, default_action?, options?, goal?)`. It never starts a turn: it lands one `escalation` row in the DM thread of the member that owns the caller (a worker's creating member; a plain session's own transcript), mirrors it onto the bell bus with a per-goal group_key, and returns while the caller keeps working. Its own tool, not a reserved `target` of `session_send` (review): a send delivers text the target RUNS as a turn, an escalation writes a row and runs nothing, and a shared name made their policy inseparable. Each policy site now classifies the verb on its own: in SESSION_CONTROL_TOOLS (caller identity), in CHANNEL_AGENT_BLOCKED_TOOLS (on send_notification's grounds -- the bell mirror -- with the backend's linked/mirrored-caller gate agreeing), granted to members (writes only into their own thread), mounted-but-gated for the conductors, own schema in MCP_DASHBOARD_SCHEMAS. `session_send` is back to `target` + `message`; an escalation field on it is refused as unknown, never dropped. The per-member conversation index (crew_conversation.py, #8612) rides here with its consumer: pending records written before the card under a pre-minted row id, `needs_you` derived and projected on the slots frame and the roster, replies matched by `meta.escalation_id` or, for typed text, by exactly-one-pending at the row's position, restore-time reconciliation against the transcript. `options` is the card's contract (#8614 renders it); until then the row is a plain line and the options are readable in the bell mirror, not clickable -- the tool description promises exactly that. An `escalation` row ends the follow-up pill scan so a previous turn's `[OPTIONS:]` chips can never post as the escalation's answer.
Summary
A crew member's DM thread is a conversation between one human and one member, and its lifetime is longer than any single session (the DM slot can be rebuilt or re-bound; a worker the member dispatched can hand a result back). This PR gives that conversation an identity of its own — kept deliberately thin so it can never disagree with the transcripts it points at.
kiro_crew.crew_conversationstores$KIROCREW_HOME/members/<slug>/conversation.json:ref{session_key, mid, role, ts}escalation{id, session_key, mid, from_session, state, deadline, default_action, goal, options, …}Design points (the spec,
docs/system-specs/modules/crew-conversation.md, pins them):(session_key, mid).midis minted once and survives restore, so the pointer is stable.needs_youis derived, not stored. Pending escalation records →needs_you. A passed deadline reads asdefaulted(a default action was declared) orexpiredwithout a write; the human's reply marks every pending recordanswered. Nothing needs to fire at a deadline — the member that set it acts on its own default.dm:<slug>today; the record carriesparticipantsandsessionslists so a latergoal:<id>conversation is a new key shape, not a schema migration.activity.jsonlin the member's own directory; the keystone-gated binding stays strict-shape identity, this is mutable UI state.Reads cache per slug on
(mtime_ns, size)because the slot projection (next PR) asks on every sidebar push. Entries are capped at 500 (~100 KiB). Deadline parsing accepts ISO-8601 or30m/2h/1d, bounded 1 minute – 7 days.No caller yet: the escalation PR stacked on this one writes and reads it.
Tests
test/test_crew_conversation.py— scaffold on missing/unreadable file, pointer-not-body on disk, participants/sessions bookkeeping,needs_youset/clear on reply, lazy deadline sweep without write,defaultedvsexpired, entry cap, deadline parsing (durations, ISO with offset, out-of-window refusals).Stack
fix/member-dispatch-home(base of this branch)feat/crew-conversation-indexfeat/escalation-user-peer—session_send target="user",needs_youprojection, spec constraintsfeat/crew-chat-profile— Crew Members chat projection + escalation cards (frontend)