Skip to content

feat(members): thin per-member conversation index - #8612

Merged
iamwhatever merged 1 commit into
fix/member-dispatch-homefrom
feat/crew-conversation-index
Sep 5, 2026
Merged

feat(members): thin per-member conversation index#8612
iamwhatever merged 1 commit into
fix/member-dispatch-homefrom
feat/crew-conversation-index

Conversation

@CrysisDeu

Copy link
Copy Markdown
Collaborator

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_conversation stores $KIROCREW_HOME/members/<slug>/conversation.json:

Entry Shape Meaning
ref {session_key, mid, role, ts} "this transcript row belongs in the conversation" (rows from a session other than the DM slot)
escalation {id, session_key, mid, from_session, state, deadline, default_action, goal, options, …} one escalation to the human and where it stands; the text stays on the transcript row

Design points (the spec, docs/system-specs/modules/crew-conversation.md, pins them):

  • Pointers, never bodies. A message body lives in exactly one place — the session JSONL — reached through (session_key, mid). mid is minted once and survives restore, so the pointer is stable.
  • needs_you is derived, not stored. Pending escalation records → needs_you. A passed deadline reads as defaulted (a default action was declared) or expired without a write; the human's reply marks every pending record answered. Nothing needs to fire at a deadline — the member that set it acts on its own default.
  • Multi-member ready. Key is dm:<slug> today; the record carries participants and sessions lists so a later goal:<id> conversation is a new key shape, not a schema migration.
  • Not the trust binding. Lives beside activity.jsonl in 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 or 30m / 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_you set/clear on reply, lazy deadline sweep without write, defaulted vs expired, entry cap, deadline parsing (durations, ISO with offset, out-of-window refusals).

Stack

  1. fix(members): dispatch server carries the gateway home override #8611 fix/member-dispatch-home (base of this branch)
  2. this PR feat/crew-conversation-index
  3. feat/escalation-user-peersession_send target="user", needs_you projection, spec constraints
  4. feat/crew-chat-profile — Crew Members chat projection + escalation cards (frontend)

@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 5, 2026 01:10
@CrysisDeu
CrysisDeu requested review from cixuuz and removed request for a team September 5, 2026 01:10
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 7318305187dff087d4acae1838e8f5143132907e — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • Spec ↔ code divergence on the hot path. The spec and description both say needs_you "reads a per-slug cache … keyed on the file's (mtime_ns, size) — a stat per call", but the code has no stat at all: _PENDING_CACHE is refreshed only by writers and prime(). The mechanisms fail differently — the documented one is self-healing; the implemented one reads False for any never-primed slug (pinned by test_cold_process_reads_false_until_primed), so after a gateway restart every pending escalation is a dark badge until a prime() call that lives in the next PR. If that PR wires the projection but misses prime at slot restore, nothing goes red. Fix the spec in this PR to describe the real mechanism and name the prime-at-restore obligation explicitly.
  • Spec asserts unlanded surface as fact. GET /api/members/{slug}/conversation and the _ChatSlot.append reply hook are documented in present tense but exist only in stacked PRs refactor: remove dead legacy compatibility shims #3–4; if the stack stalls, the module spec describes endpoints that don't exist. Mark them as landing with the escalation PR.

Suggestions

  • Consider actually implementing the (mtime_ns, size) stat cache the spec describes: one cheap stat per push removes the prime() coupling entirely and makes restart correctness structural rather than a caller obligation.

[DESIGN-REVIEWED] 7318305

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 7318305187dff087d4acae1838e8f5143132907e and found no blocking issues.

This comment is updated in place on each push.

Review details

FINDING -- src/kiro_crew/crew_conversation.py:350 -- distinct "from_session" values never enter sessions, leaving worker escalations internally inconsistent -> Fix: also call _ensure_session(record, from_session).
FINDING -- docs/system-specs/modules/crew-conversation.md:42 -- "(mtime_ns, size)" caching, projections, and API routes are documented but absent; the code uses a manually primed memory cache with no callers -> Fix: document current behavior and defer follow-up surfaces. (origin: validation)
[GPT-REVIEWED] 7318305

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 7318305187dff087d4acae1838e8f5143132907e: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 7318305187dff087d4acae1838e8f5143132907e — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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 ships

Intent: 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.

  1. New per-member conversation.json of pointers + escalation records — justified, zero consumers (declared stacked)
  2. Escalation lifecycle pending→answered/defaulted/expired, lazy sweep, no poller — justified
  3. needs_you badge state via memory-only cache + prime() — framing contradicted (see Watch)
  4. ref entries / append_ref for foreign-session rows — zero consumers, unclaimed by the declared stack
  5. Deadline parsing 30m/2h/ISO, 1 min–7 day window — justified; 4th module-local duration map
  6. participants/sessions lists shaped for future multi-member — inherited ("so we can later"), one shape ever constructed
  7. 500-entry cap that never evicts a pending decision — justified
  8. retract_escalation compensation for a failed card append — consumer in stacked PR
  9. Spec doc + README row documenting endpoint/hook/projection that don't exist yet — undeclared as future
  10. invalidate_cache — tests-only consumer

Watch

  • Description ("Reads cache per slug on (mtime_ns, size)") and spec ("a stat per call, a parse only when the file changed") describe a mechanism the code explicitly forbids — needs_you docstring: "it must not stat or read a file"; the shipped cache is writer-maintained memory requiring prime(). A spec born disagreeing with its module violates the same-commit sync invariant.
  • The spec states GET /api/members/{slug}/conversation, the _ChatSlot.append reply hook, and roster/slots projection as present behavior; grep for crew_conversation importers in src/: 0. Readers of the spec will look for surface that isn't there until the stacked PRs land.
  • append_ref and the ref entry type have zero consumers and the declared stack (refactor: remove dead legacy compatibility shims #3 escalation, fix(app-sdk): harden scoped-API guard against path traversal and 204 bodies #4 frontend) names no ref writer — speculative until one is named.
  • _DURATION_UNITS is the 4th local duration map (mcp_cron.py:70, cli_commands.py:1394, pod/cli.py:597); a shared helper is a larger fix, accepted-and-deferred.

Subtractions

  • Delete the (mtime_ns, size) cache paragraph in docs/system-specs/modules/crew-conversation.md (Storage section) and the matching description sentence — the mechanism doesn't exist.
  • Shrink the spec's "Read surface" / reply-hook / projection passages to what this PR ships; those sections belong to the PRs that add them.
  • Replace the participants list with the member slug/name pair it always holds — one constructed shape, zero behavioral readers; dm: vs goal: key shapes already carry the future distinction.

[FIRST-PRINCIPLES-REVIEWED] 7318305

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 7318305187dff087d4acae1838e8f5143132907e — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 7318305

Verdict parsed from the review's SHA-scoped output markers for commit 7318305187dff087d4acae1838e8f5143132907e.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 7318305187dff087d4acae1838e8f5143132907e: <one-sentence reason>

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/member-dispatch-home branch from c946e29 to f81f043 Compare September 5, 2026 01:50
@CrysisDeu
CrysisDeu force-pushed the feat/crew-conversation-index branch from 3be88bd to 0675595 Compare September 5, 2026 01:50
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/crew-conversation-index branch from 0675595 to 1d1628e Compare September 5, 2026 02:07
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/member-dispatch-home branch from f81f043 to 50c9b25 Compare September 5, 2026 02:27
@CrysisDeu
CrysisDeu force-pushed the feat/crew-conversation-index branch from 1d1628e to 032d9e7 Compare September 5, 2026 02:27
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Sep 5, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • Read → mutate → atomic_write with no lock — fixed. Every mutator (append_ref, record_escalation, mark_answered, retract_escalation) now runs its whole read-modify-write under a per-slug threading.Lock; a 4-writer × 20-entry concurrency test asserts no entry is lost.

The writers are all threads of one gateway process (the dashboard's executor), which is exactly where last-writer-wins bites; the lock is the fix, and the spec now states the single-process assumption explicitly.

@github-actions github-actions Bot added the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 2026
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.
@CrysisDeu
CrysisDeu force-pushed the fix/member-dispatch-home branch from 0921723 to f76fe5f Compare September 5, 2026 03:19
@CrysisDeu
CrysisDeu force-pushed the feat/crew-conversation-index branch from f490176 to 7318305 Compare September 5, 2026 03:19
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Sep 5, 2026
@iamwhatever
iamwhatever merged commit a2b587e into fix/member-dispatch-home Sep 5, 2026
18 checks passed
@iamwhatever
iamwhatever deleted the feat/crew-conversation-index branch September 5, 2026 06:53
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 2026
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
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.
CrysisDeu added a commit that referenced this pull request Sep 5, 2026
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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants