# Master Chat — scoping (read-only investigation) **Feature target.** A human asks the OpenHuman agent a question in a "Master chat". OpenHuman either (a) answers from its own persisted session history with other agents, or (b) asks an external agent on the human's behalf by DMing it under a session id (new or existing), with the external reply threading back into the Master-chat answer. **Grounding.** All `EXISTS` refs are against `upstream/main` (`git show upstream/main:`, fetched at investigation time — `upstream/main` @ `f49249360`). In-flight work accounted for: **PR #4582 is already MERGED into main** (shared per-pair `session_key`, plus the warn-only seq guard); **issue #4583 is OPEN** (robust monotonic ingest cursor — *not* yet implemented, only the interim warn shipped); **plugin PR tiny.place#227** (one shared session id per thread, reused on reply) is the peer-side counterpart the merged core code already assumes. > Terminology caution: in the code today, **"master" (`ChatKind::Master`)** is the window > that aggregates *plain (non-envelope) DMs* from peers, and `orchestration_send_master_message` > is the human→front-end **steering** send. The task's "Master chat" (human asks OpenHuman, > OpenHuman answers *back to the human*) is a **superset** that is only partially realized by > this window — see the gap analysis. --- ## WHAT EXISTS TODAY Domain root: `src/openhuman/orchestration/` (declared in `mod.rs:16-34`; enabled behind `config.orchestration.enabled`, schema `src/openhuman/config/schema/orchestration.rs:39`). Startup wiring: `src/core/jsonrpc.rs:2294-2300` (ingest subscriber, wake subscriber, drain supervisor). ### 1. Master vs Session windows model - **`ChatKind`** — `Master | Subconscious | Session` (`types.rs:113-155`, `as_str`/`from_str` with Master as the safe default at `:150`). - **`SessionEnvelopeV1`** — hand-rolled mirror of the tiny.place harness envelope (`types.rs:79-96`); `is_valid_v1` requires `envelope_version == "tinyplace.harness.session.v1"` and a non-empty `harness_session_id` (`types.rs:99-104`); `parse` returns `None` for any non-envelope body so it routes to Master (`types.rs:108-112`). - **`session_key()`** (`types.rs:123-135`) — **PR #4582, on main**: returns `scope.wrapper_session_id` (the shared per-pair conversation id both peers stamp on every message for a thread), falling back to `harness_session_id` only for a legacy envelope. This is the sole inbound routing key. - **`SessionEnvelopeV1::outgoing()`** (`types.rs:139-162`) — builds a v1 envelope with both `wrapper_session_id` and `harness_session_id` set to the same `session_id`, `role="owner"`, so a compliant peer threads its reply under the same id. - **`classify_message`** (`ingest.rs:44-90`) — pure classifier: a parseable envelope → `ChatKind::Session` keyed by `session_key()` (`ingest.rs:47-75`, seq = `env.message.line`, source = `harness.provider`, label from folder scope, workspace from cwd); anything else → `ChatKind::Master`, `session_id="master"`, `role="user"`, `seq=0` (`ingest.rs:76-88`). - **Persisted rows** — `OrchestrationSession` (`types.rs` `sessionId/agentId/source/label/ workspace/lastSeq/...`) and `OrchestrationMessage` (`id/agentId/sessionId/chatKind/role/ body/timestamp/seq`); `body` is decrypted plaintext ⇒ workspace-internal only. ### 2. Ingest → persist → wake pipeline - **Bus subscribers** (`bus.rs`): `OrchestrationIngestSubscriber` (domain `tinyplace`, `bus.rs:60-92`) feeds `ingest_stream_message`; `OrchestrationWakeSubscriber` (domain `agent`, `bus.rs:112-133`) fans each persisted `OrchestrationSessionMessage` to the renderer socket **and** calls `ops::schedule_wake` — for **all** kinds except subconscious. - **`ingest_one`** (`ingest.rs:171-247`): 1. **linked-sender gate** — only DMs from paired (linked) agents are decrypted; unpaired senders are skipped *without* decrypt/ack so they stay in the Messaging UI mailbox (`ingest.rs:187-198`). 2. **dedupe-before-decrypt** by relay `msg_id`, protecting the non-idempotent Signal ratchet; a duplicate re-acks but never re-decrypts (`ingest.rs:200-217`). 3. decrypt-once → `classify_message` → `persist_message` (`ingest.rs:219-224`). 4. on a new row: `acknowledge_message` + `publish_global(OrchestrationSessionMessage{...})` (`ingest.rs:226-242`). - **`persist_message`** (`ingest.rs:92-149`): upserts the session row and inserts the message idempotently. Contains the **#4583 interim guard** — if `chat_kind == Session` and the inbound `seq <= existing last_seq`, it logs a `warn` (`ingest.rs:107-127`) but still persists (proved by test `persist_still_stores_a_lower_seq_in_the_same_wrapper_session`). - **Delivery is poll-only**: `drain_mailbox_once` (`ingest.rs:264-310`) lists `/messages` every 15 s via `start_message_drain_supervisor` (`ops.rs:129-145`) — relay DMs never hit the `/inbox/stream` WS, so the poller is the real inbound path. - **`schedule_wake`** (`ops.rs:83-118`): per-session debounce via a generation counter (`ops.rs:58-78`); subconscious kind returns early (`ops.rs:96-98`); the last trigger within `debounce_ms` wins → `invoke_orchestration_graph`. - **Per-session idempotence cursor** (`ops.rs:51-54`, `192-211`): `has_new_work` compares `latest_seq` against `kv["cursor:{agent}:{session}"]`; `advance_cursor` moves it **only after a completed, DM-sent cycle** (`ops.rs:518-522`). Store: `session_last_seq` (`store.rs:219`), `upsert_session` clamps `last_seq = MAX(old,new)` (`store.rs:169`), `ingest_cursor_lag` (`store.rs:385`). **There is no monotonic `next_session_seq` on main** (confirmed absent) — cursor and lag both key on `env.message.line`, which is the #4583 defect. ### 3. Wake graph nodes (what runs the LLM) - **Graph shape** (`graph/mod.rs:1-30`, wired in `graph/build.rs:328-345`): `normalize → frontend → execute → compress → world_diff → frontend → send_dm → context_guard → done`. One `OrchestrationState` (`graph/state.rs:49-95`) is threaded and SQLite-checkpointed per super-step under thread `orchestration:` (`build.rs:351-383`). - **Injected `OrchestrationRuntime` seam** (`build.rs:44-61`) — every LLM/effect op is behind this trait; production impl `ProductionRuntime` in `ops.rs:550-817`. - **`frontend` (router)** (`build.rs:149-211`): two-pass Quick-LLM. Pass 1 → `frontend_instruct` (`ops.rs:595-604`) → macro-instructions → `execute`. Pass 2 (once `agent_reply` present) → `frontend_compile` (`ops.rs:606-617`) → `channel_response` → `send_dm`. `max_supersteps` backstop guarantees termination (`build.rs:166-181`). Agent package: `frontend_agent/` (`prompt.md`, `agent.toml` tier `chat`, tools `defer_to_orchestrator` + `reply_to_channel` from `tools.rs`). - **`execute` (reasoning core)** (`build.rs:217-231` / `ops.rs:619-640`): runs `reasoning_agent` with the current steering directive scoped in (`reasoning_agent/steering.rs`, `ops.rs:626-633`); produces `reply` + a `trace`. Package `reasoning_agent/agent.toml`: tier `reasoning`, subagents `researcher/code_executor/tools_agent`, tools **`spawn_async_subagent, wait, tinyplace_whoami, tinyplace_status, tinyplace_feed, current_time, resolve_time`** — i.e. only tiny.place **read** tools; **no DM-send tool, no session-mint tool.** - **`compress`** (`ops.rs:642-707`): 20:1 summary of the trace → `compressed_history` row (`store.rs:474`). - **`world_diff`** (`ops.rs:709-736`): append-only per-`(agent,session)` mutation timeline (`store.rs:515` `append_world_diff`, monotonic `seq`). - **`context_guard`/`evict`** (`ops.rs:738-802`): utilization estimate; over threshold, evicts the oldest compressed summaries into memory-RAG under `path_scope = orchestration/` (`ops.rs:764-784`). - **`send_dm`** (`build.rs:273-294` / `ops.rs:804-816`): sends `channel_response` back to **`state.counterpart_agent_id`** (the session's peer). `dm_sent` latch prevents double-send. ### 4. The SEND path (OpenHuman → peer under a sessionId) Two send surfaces exist, both routing through `tinyplace::handle_tinyplace_signal_send_message`: - **Graph reply** — `ProductionRuntime::send_dm` (`ops.rs:804-816`) → `session_send_plaintext` (`ops.rs:822-835`): for a real session id it wraps the body in `SessionEnvelopeV1::outgoing` (so the peer threads its reply under the *same* id); `"master"`/`"subconscious"` stay plain. **This is a reply only — it always targets the current session's `counterpart_agent_id`; it cannot initiate a new outbound thread to a different agent or mint a new session id.** - **Human steering send** — `orchestration_send_master_message` (`schemas.rs:108-118`, handler `:437-539`): body + optional `recipient` + optional `sessionId`. Recipient resolution (`schemas.rs:457-472`): explicit wins; else the session's contact (`store::session_agent_id`, `store.rs:368`); else the latest master peer (`store::latest_master_peer`, `store.rs:356`). With `sessionId` it wraps a v1 envelope (`session_envelope_plaintext`, `schemas.rs:425-435`) and mirrors into that session window; without it, plain into `"master"`. It then `notify_orchestration_message` (`schemas.rs:535`) — which, via the wake subscriber, **wakes the local graph** on that window. - **Session mint** — `orchestration_sessions_create` (`schemas.rs:82-91`, handler `:347-380`) mints a fresh `uuid` session id for a contact, `source="user_created"`. **Renderer-driven only** — no agent-side or graph-side caller. ### 5. Session persistence + read surface + frontend - **Store reads**: `list_sessions` (`store.rs:254`), `list_messages_by_session` (`store.rs:279`, session-key aggregated, paged by `before`), `list_recent_messages` (`store.rs:441`, chronological window used by the graph), `unread_count`/`mark_chat_read` (`store.rs:329`/`339`). - **RPC surface** (`schemas.rs:23-71`): `orchestration_sessions_list`, `_sessions_create`, `_messages_list`, `_send_master_message`, `_mark_read`, `_status`, `_self_identity`, `_relay_info` — internal registry (renderer-only, never advertised to agents). - **Frontend Master-chat surface**: - Client `app/src/lib/orchestration/orchestrationClient.ts:166-200` (`sessionsList`, `messagesList`, `sendMasterMessage`, `sessionsCreate`, `markRead`, `status`, ...). - Hook `app/src/lib/orchestration/useOrchestrationChats.ts` — `sendMessage` (`:260-303`): a **session** chat sends `{ body, recipient, sessionId }`; **master/subconscious** send `{ body }` (steering to the latest master peer). Optimistic append + reconcile via `loadMessages`/`loadSessions`. - UI: `app/src/components/intelligence/TinyPlaceOrchestrationTab.tsx` + `TinyPlaceRoster`, `InstanceCard`, `InstanceStatusDot`, `HarnessGlyph`, `SelfIdentityCard`, `RelayBadge` (roster of instance-shaped sessions; recent commits 0500aa9fb / 05a3c97b9). ### 6. Is cross-agent Session-window history fed to the LLM today? **No.** The graph seeds from **one** window only: `seed_state` (`ops.rs:150-166`) calls `list_recent_messages(agent_id, session_id, message_window)` and `OrchestrationState::seed` (`state.rs:97-134`) folds just those messages + the single `counterpart_agent_id`. The reasoning core's only cross-session recall is indirect (memory-RAG over evicted compressed summaries at `orchestration/`, and the `tinyplace_feed`/`_status` read tools). There is **no** node or tool that loads/validates the OpenHuman↔agentX **session transcript(s)** as explicit context for answering a question, and none that spans *multiple* sessions/peers. --- ## WHAT'S NEEDED (gap analysis) Target flow: **human asks in Master chat → orchestration LOADS the relevant OpenHuman↔agent session history (validate persistence) → runs the LLM over that history + the question → if an external agent is needed, SEND a DM under a session id (reuse the per-pair id if a thread exists, else mint) → the external reply threads back (shared-session-id model) → update the Master-chat answer.** Wired-vs-missing summary of the 5 sub-flows. **The `State at scoping` column is the pre-PR baseline** (kept as historical context); the `Landed` column is what this PR delivers. | # | Sub-flow | State at scoping (pre-PR) | Landed in this PR | |---|----------|---------------------------|-------------------| | 1 | human→OpenHuman question intake + routing into the graph | **Partial** — `send_master_message` + master wake exist, but they reply *outbound to a peer*, not *back to the human*. No "answer the asker" surface. | ✅ W2 local-ask path + human-facing `master_agent`; answers land back in the Master window. | | 2 | graph loads + validates cross-agent session history as context | **Missing** — single-window seed only. | ✅ `orchestration_list_sessions` / `orchestration_read_session` browse tools. | | 3 | decision + tool for OpenHuman to ask an external agent | **Missing** — reasoning core has no send/ask tool. | ✅ `orchestration_send_to_agent` (linked-peers-only guardrail). | | 4 | choose new-vs-existing session id for the outbound ask | **Missing** — `sessions_create` mints; no reuse-lookup; no agent caller. | ✅ reuse `latest_session_for_agent`, else mint fresh. | | 5 | thread external reply back into the Master-chat answer | **Missing** — reply wakes the *sub-session* graph and replies to the peer; never correlated back to the originating Master question. | ✅ W7 correlation (process-global beacon) → reply surfaced as OpenHuman's own message. | Session-persistence-validity concerns to carry through: **dedupe** (already solid — `message_exists` before decrypt, `INSERT OR IGNORE`); **ordering/#4583** (cursor keyed on `env.message.line` can silently drop the 2nd+ inbound message on a shared `wrapper_session_id` — **blocks reliable reply-threading**, must be fixed before sub-flow 5 is trustworthy); **stale/dead session ids** (no liveness/expiry on session rows); **shared-id reuse rules** (#227/#4582 give inbound threading; outbound *initiation* id-choice is unspecified). ### Status update — PR #4599 (OPEN, `Closes #4583`, follow-up to #4582) #4599 lands the **reliable reactive reply loop** (`ingest.rs`/`ops.rs`/`store.rs`/`tools.rs` only). It closes the P0 foundation and adds plumbing the answer path reuses — but delivers **none** of the target-flow gaps (it makes *peer→OpenHuman→peer* replies reliable, not *human→OpenHuman→(history|external agent)→human*): - **W1 → DONE**: `store::next_session_seq` = `MAX(seq)+1` per `(agent,session)`, stamped in `persist_message` on both `last_seq` and message `seq` (replaces the warn guard); also fixes the `cycle_id` collision. - **Reply-content fix → DONE (new prerequisite for a *correct* answer)**: `tools::with_decision_capture` task-local captures the `reply_to_channel` / `defer_to_orchestrator` payload; `frontend_instruct`/`frontend_compile` prefer it over `run_single`'s trailing narration. - **Answer-display plumbing → DONE (reused by W7/W10)**: `ProductionRuntime::persist_outgoing_reply` writes the reply `role=owner` (monotonic seq) + `notify_orchestration_message` → the reply now renders in `orchestration_messages_list` / the chat window. - **Wake resilience → DONE**: `schedule_wake` retries with 5s/15s/45s backoff, checkpoint-resumed, bails if superseded. Three follow-ups #4599 explicitly defers (its "Related") are folded in below as **F1/F2/F3**. ### Design pivot — agentic tools, not static seed (branch `feat/master-chat-orchestration-tools`) Per the owner: the master orchestration layer should have **tools** to (1) browse its OpenHuman↔agent session chats and (2) send messages on OpenHuman's behalf — rather than the graph pre-loading history into the prompt. This **replaces W3/W4** ("seed history into state") with on-demand read tools the reasoning core calls, and reframes W5 as the send tool. **Shipped in this branch (read slice):** - ✅ `orchestration_list_sessions` + `orchestration_read_session` read tools (`orchestration/tools.rs`), registered in `tools/ops.rs`, added to the `reasoning_agent` allowlist + prompt nudge. Read-only, concurrency-safe, workspace-internal store access via `store::{list_sessions,count_messages,list_recent_messages,list_messages_by_session}`. Delivers target half (a): answer from own history. **Shipped in this branch (send slice — W5 + W6):** - ✅ `orchestration_send_to_agent` (`orchestration/tools.rs`) — DM a peer on OpenHuman's behalf. **Guardrail: linked-peers-only** (`pairing::linked_agent_ids` OR an existing session with the peer) — refuses cold-DMs from the un-gated background origin. **Session id: reuse-or-mint per peer** via new `store::latest_session_for_agent` (reuse the peer's newest thread's shared `wrapper_session_id` so the reply threads back; else mint a uuid). Sends a v1 envelope via `handle_tinyplace_signal_send_message`, records the outbound `role=owner` message + `notify_orchestration_message` (mirrors #4599's `persist_outgoing_reply`). `PermissionLevel::Write`. Added to the `reasoning_agent` allowlist + prompt. - Verified: `cargo test openhuman::orchestration` → 64/64 (6 new); loader 85/85; lib clean. **Shipped in this branch (reply-threading — W7, core-only):** - ✅ One-shot outbound-ask correlation. For a local-master turn the `execute` node opens a **process-global origin beacon** (`tools::begin_master_origin`/`end_master_origin`) around the agent turn; `orchestration_send_to_agent` reads it and records `store::set_pending_ask((peer_agent, ask_session) → origin)`. A process-global is used instead of a task-local because the harness dispatches tool calls past an internal `tokio::spawn`, and task-locals do **not** cross a spawn — the earlier `with_origin_session` task-local silently never reached the tool, so the correlation never armed. The key is scoped by `(peer_agent, session)` so a legacy shared `wrapper_session_id` across peers can't misroute a reply. When the peer's reply lands, `invoke_with_runtime` correlates it and **finishes the cycle without running the reply graph** — no ping-pong. One-shot: the pending marker is consumed **only after the reply is durably surfaced** (`store::{pending_ask_origin,clear_pending_ask}`), so a transient store failure retries on the next drain instead of dropping the answer. - ✅ Reply surfaced as **OpenHuman's own message**, not the peer's raw words. For a master-initiated ask the reply is run through the **tool-free `master_reporter`** (`report_peer_reply_to_master`) — the peer text is untrusted, so the reporter carries no tiny.place tools/sub-agents (no prompt-injection surface) and emits an `assistant` message in the Master window. Peer/A2A origins keep the deterministic raw `thread_reply_to_origin`. - ✅ Fire-and-forget: `orchestration_send_to_agent` returns an immediate ack and the `master_agent` prompt forbids polling/`read_session` for the reply, so W7 is the sole async reporter (no duplicate surfacing). - Verified: `cargo test openhuman::orchestration` green (incl. `outbound_ask_reply_threads_to_ origin_and_skips_the_reply_graph`, `pending_ask_correlation_is_one_shot`, `master_origin_beacon_sets_and_clears`); full loop proven live on staging. - **Limitation (needs F3):** correlation is a pragmatic 1:1 request/response — it assumes the *next* inbound message on the ask session is the answer. Many-in-flight / interleaved replies need an explicit envelope `inReplyTo`/`fromSession` (F3, cross-repo). The process-global beacon is safe because local-master wakes are serialized; a concurrent A2A send during a master turn is the one documented edge (single-user desktop, non-fatal). Peer-session (A2A) W7 still rides the best-effort task-local. **Still to do here:** RPC/UI surface for the human-facing master ask/answer (W8–W12) and perf/robustness **F1/F2**; robust correlation **F3** (cross-repo). ### Prioritized work-item CHECKLIST (ordered Rust core → JSON-RPC → UI → tests) — remaining after #4599 **P0 — foundation / correctness** - [x] **W1 — Fix the #4583 wake-cursor/seq decoupling. — DONE in #4599** (`store::next_session_seq` stamped in `persist_message`). Note: #4599 keys `last_seq`/message `seq` on the ordinal; `has_new_work`/`advance_cursor`/`ingest_cursor_lag` already ride `last_seq`, so they inherit the fix. No remaining work. - [ ] **W2 — Master-chat "ask" intake distinct from steering.** Introduce a question record (role `owner`, a `pending`/answered marker) and route it into the graph as *the question to answer for the human*, separate from `send_master_message`'s outbound peer-steering. *Where:* `orchestration/{types.rs,store.rs,ops.rs,schemas.rs}` (+ maybe a `master` chat mode). *Size:* **M.** *Deps:* W1. **P1 — the answer path** - [ ] **W3 — Cross-agent session-history loader + validity check.** A store read that gathers the relevant OpenHuman↔agent transcript(s) (by peer and/or across peers), validates ordering (monotonic seq from W1), drops dead/stale sessions, and returns a bounded, LLM-ready context. *Where:* `orchestration/store.rs` (new `load_history_for_question`) + `ops.rs::seed_state` (extend state with a `history_context`). *Size:* **M/L.** *Deps:* #4599 (monotonic seq). - [ ] **W4 — Feed the history into the reasoning/front-end prompt.** Extend `OrchestrationState` (`graph/state.rs`) with the loaded history and render it in `frontend_instruct` / `execute` prompts (`ops.rs:595-640`). *Where:* `orchestration/graph/state.rs`, `ops.rs`. *Size:* **S/M.** *Deps:* W3. - [ ] **W5 — "Ask an external agent" decision + tool.** A new domain tool (e.g. `ask_external_agent { recipient, question, sessionId? }`) that the reasoning core can call; it performs the outbound session-scoped send. Add to `reasoning_agent/agent.toml` allowlist and gate it (approval/linked-only). *Where:* `orchestration/tools.rs` (+ re-export in `tools/mod.rs`), `reasoning_agent/agent.toml`, reuse `session_send_plaintext`/ `handle_tinyplace_signal_send_message`. *Size:* **M.** *Deps:* W2. - [ ] **W6 — New-vs-existing session-id chooser for the outbound ask.** Resolve "do I already have a live thread with peer X?" → reuse that `wrapper_session_id`; else mint (uuid, per `sessions_create`) and record the `(peer → session_id)` mapping. Encode the #227/#4582 reuse rule (one shared id per thread; both peers reuse on reply). *Where:* `orchestration/store.rs` (a `find_or_create_session_for_peer`), consumed by W5. *Size:* **M.** *Deps:* W5. - [ ] **W7 — Thread the external reply back into the Master-chat answer.** Correlate the inbound session reply (arriving under the shared id, `ChatKind::Session`, waking that sub-session's graph) back to the originating Master question, and update/emit the Master-chat answer instead of (or in addition to) replying to the peer. Needs a pending-ask ↔ session-id correlation table and a resume/notify into the master window. Reuse #4599's `persist_outgoing_reply`/`notify_orchestration_message` for surfacing the answer; the correlation primitive is **F3** (envelope `inReplyTo`/`fromSession`). *Where:* `orchestration/store.rs` (correlation kv/table), `ops.rs` (wake handling for a correlated session), `bus.rs`. *Size:* **L.** *Deps:* W2, W5, W6, **F3**. *Risk:* highest — cross-graph correlation + the reactive `send_dm`-always-to-counterpart assumption in `build.rs:273-294` must be relaxed. **P2 — JSON-RPC surface** - [ ] **W8 — RPC for master-chat ask + answer polling.** e.g. `orchestration_ask_master` (submit question) and answer surfacing via existing `messages_list`/socket, plus a pending status. *Where:* `orchestration/schemas.rs` (new schema + handler; register in `all_*controllers`). *Size:* **M.** *Deps:* W2, W7. - [ ] **W9 — Extend `orchestration_status`/session DTOs** with pending-ask + external-ask in-flight signals for the UI. *Where:* `schemas.rs`. *Size:* **S.** *Deps:* W8. **P3 — UI** - [ ] **W10 — Master-chat ask/answer UX.** Client method + hook wiring so a master question shows "OpenHuman is asking @peer…" and the threaded answer updates in place. *Where:* `app/src/lib/orchestration/orchestrationClient.ts`, `useOrchestrationChats.ts`, `TinyPlaceOrchestrationTab.tsx`. *Size:* **M.** *Deps:* W8. *Note:* i18n keys in `en.ts` + all locales; no dynamic imports. **P4 — tests (each item lands with its own unit tests; these are the cross-cutting suites)** - [ ] **W11 — Rust: threading correctness.** (a,b) seq/cursor line-0 + reuse cases are already covered by #4599 (`persist_stamps_monotonic_ingest_seq_so_line_zero_dms_still_wake`) — only (c) remains: outbound ask reuses an existing per-pair id and the reply correlates back to the originating master question. *Where:* `orchestration/{ops.rs,store.rs}` inline tests + `tests/json_rpc_e2e.rs`. *Size:* **M.** *Deps:* W6, W7. - [ ] **W12 — JSON-RPC E2E + Vitest** for ask→answer round-trip (mock peer reply). *Where:* `scripts/test-rust-with-mock.sh`, `app/src/**/*.test.tsx`. *Size:* **M.** *Deps:* W8, W10. **F — deferred by #4599 (fold into the target work)** - [ ] **F1 — `send_dm` before `compress`/`world_diff`** to cut ~20s time-to-reply (reorder the graph edges so the outbound reply dispatches before the post-processing tail). *Where:* `orchestration/graph/build.rs` (edge wiring). *Size:* **S.** *Deps:* none. *Note:* perf, not blocking, but improves the human-facing answer latency the target cares about. - [ ] **F2 — Scope the checkpoint thread id to `(agent, session)`.** Today it is `orchestration:` (`build.rs:358`), so two peers sharing a session id can collide on checkpoints. *Where:* `orchestration/graph/build.rs::run_orchestration_graph`. *Size:* **S.** *Deps:* none. *Correctness* — do alongside W6/W7 (which multiply active sessions per id). - [ ] **F3 — `SessionEnvelopeV1` `inReplyTo` / `fromSession` correlation.** The primitive W7 needs to tie an inbound reply to the exact outbound ask (vs. inferring from session id alone), and to drive a `send_and_wait`-style block. *Where:* `orchestration/types.rs` (`SessionEnvelopeV1`), plus **tiny.place SDK / plugin #227 peer coordination**. *Size:* **M** (core) **+ external.** *Deps:* peer/SDK support. *Risk:* cross-repo — the blocking dependency for a clean W7. --- ## Open questions / risks 1. **Semantic overload of "master".** The window is currently peer-plain-DM aggregation + human steering; the target needs a human-question/answer channel. Decide whether to reuse `ChatKind::Master` or add a mode — affects W2/W7 and the wake router's terminate predicate. 2. **`send_dm` is unconditional-to-counterpart** (`build.rs:273-294`). Sub-flow 5 requires the graph to sometimes answer the human instead of/along with the peer — a structural change to the terminal node, not just a new tool. 3. **#4583 silent-drop — RESOLVED by #4599** (`next_session_seq` monotonic ordinal). Build reply-threading on `last_seq`/message `seq`, not the wire `line`. 4. **Outbound id-choice reuse rule is peer-side (#227) only in code.** The core has no "find existing thread with peer X" lookup; W6 must define freshness/liveness (ties to the stale-session-id concern — session rows have no expiry today). 5. **`env.message.line` is now ignored for ordering** (#4599). Its value no longer matters to the wake cursor; correlation (W7) must ride the store seq / an explicit `inReplyTo` (F3), not `line`. 6. **Approval/security:** an agent-initiated outbound DM (W5) is a new external-effect tool; must respect the linked-agent gate (`ingest.rs:187-198`) and the approval gate — scope its `CommandClass`/tier deliberately.