mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
401 lines
27 KiB
Markdown
401 lines
27 KiB
Markdown
# 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:<path>`,
|
||
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:<session_id>`
|
||
(`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/<session>`
|
||
(`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/<session>`, 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:<session_id>` (`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.
|