feat: implement group chat and chat store with MCP server integrations#7
Open
fannnzhang wants to merge 359 commits into
Open
feat: implement group chat and chat store with MCP server integrations#7fannnzhang wants to merge 359 commits into
fannnzhang wants to merge 359 commits into
Conversation
These vars were used by the backend in plan 05's backend-assembled-QR model. ADR 0016 superseded that — clients now have CF Access tokens and backend URL baked at build time. The vars haven't been read by backend source for a while. - error.rs fixture: drop the MINOS_BACKEND_ prefix on the CF var name in the CfAccessMisconfigured display test. - cloudflare-tunnel-setup.md §7a: replace the dead-var configuration block with a clear note that the backend doesn't need them. Historical references in docs/adr/0014 and docs/superpowers/plans/05 get superseded-by banners in Phase 8.
Both crates' build.rs gain release-mode panic and debug-mode cargo:warning when MINOS_BACKEND_URL is not in the build environment. Combined with Phase 5's Pre-Build hook this is two layers of guard against the localhost-baking bug: - Layer 1 (Xcode): Pre-Build script fails the build if MINOS_BUILD_VIA_JUST=1 is absent (catches IDE Build/Run). - Layer 2 (cargo): build.rs panics in release if MINOS_BACKEND_URL is absent (catches CLI invocations that bypassed just but somehow set MINOS_BUILD_VIA_JUST=1). Debug builds still emit a visible warning so the silent localhost fallback is never invisible. cargo auto-discovers build.rs at package root; no Cargo.toml edit needed for the new daemon build.rs.
- README.md: rewrite Local setup around `cp .env.example .env.local` and just recipes; remove direct flutter run / Xcode IDE Build paths (now blocked by the Pre-Build guard). - apps/mobile/README.md: same shape, points readers at workspace just. - docs/adr/0018-just-config-pipeline.md: new ADR documenting the decision (single .env.local, justfile entry point, Pre-Build hook, release fail-fast, secret hygiene). - docs/ops/secrets-rotation.md: runbook for CF Access service token rotation and JWT secret rotation. - ADRs 0013, 0014, 0016: status banners pointing at 0018. - justfile: add rotate-cf-access recipe (cats the runbook).
The new build.rs (Phase 7) emits a cargo:warning on debug builds when MINOS_BACKEND_URL is unset. Pinning the dev-fallback URL at the job level keeps the warning quiet without changing build behavior — both linux and macos lanes do debug builds only today, and minos_domain's DEV_BACKEND_URL constant is exactly this string. The trailing release-job TODO comment (previously plan-05-flavored) is rewritten to use `just build-mobile-ios Release` per ADR 0018, including the secret-injection shape and the PR-from-fork guard.
Bundled snapshot of feature/mobile-auth-and-agent-session local work: - mobile (flutter): app_shell_page + pairing_page rewrite; drop the top-level `pairing` route in favour of an entry-point inside the shell; rename ConnectionState UI labels and the matching `threadListOffline` enum (formerly `threadListMacOffline`) so they describe the device↔server link rather than the Mac peer (mobile has no PeerState wiring — Event::PeerOnline/PeerOffline are dropped in recv_loop, so the old "Mac …" labels were misleading). - mobile (rust core): client.rs surface tweaks, minos.rs additions, FRB bindings regenerated. - daemon: relay_client + keychain_store adjustments. - agent-runtime: exec_jsonl extensions. - macos app: AppState + MenuBarView + DaemonBootstrap follow-ons. - justfile + docs (ADR 0018, README, unified-config-pipeline-design): config pipeline polish.
Mirror the existing apps/mobile/.fvm/ rule for the workspace-root FVM config, and exclude third_party/ which holds local checkouts of fire, openwire, and remodex used as design references.
Pre-existing test that newer dart format wants collapsed onto one test() call — applied so check-all stops failing on this file.
Stopping an exec session now snapshots a ResumableExec (agent + started_at + codex_session_id) keyed by thread id, so a subsequent send_user_message into that thread re-mints the active session instead of returning AgentNotRunning. Drop the synthetic thread/archived event so a stopped thread can be revived seamlessly. The backend's raw_events store grows insert_assigning_seq, which keeps exact-payload retransmits idempotent but appends at a fresh backend seq when a resumed daemon reuses a process-local seq with new payload — otherwise restart-after-resume silently drops new Codex output. Also satisfies clippy 1.95 warnings carried over from the prior checkpoint (unnested_or_patterns + too_many_lines on normalize_event_msg/normalize_response_item and the e2e resume test).
Migration 0010 drops the single-pair-per-device triggers; one Mac may now be paired with multiple iOS clients and one iOS may pair multiple Macs. Removes the spec §10.2 R4 "refuse if already paired" check — the only remaining PairingStateMismatch is self-pairing. Forget paths clear device secrets only when the side has no remaining pair AND is not an AgentHost (one Mac stays online for the other clients). Login no longer revokes other devices on the same account: introduces refresh_tokens::revoke_all_for_device so login rotates only the caller's own tokens, and stops force-closing other iOS WS sessions in the registry. close_account_sessions remains for explicit admin/revocation flows. Pair-fanout follows: ingest::broadcast_to_peers_of iterates every peer returned by pairings::get_peers; envelope::handle_forward routes Mac JSON-RPC replies back to the originating iOS by tracking (request_id -> requester) on the AgentHost handle, since paired_with no longer uniquely identifies a peer. Cargo.lock drops the now-orphaned rustls-pemfile transitive.
…are UI ActiveSessionController grows sendToThread(threadId, agent, text) so the thread page can send into a known thread id no matter the global state. ThreadViewPage stops always reusing the live session: when the user opens a historical thread, the page renders against a view-scoped ActiveSession that's only "live" when the session is bound to that same thread id. Combined with the daemon's resumable-exec change, follow-ups into a stopped thread call send_user_message(session_id) and the daemon re-mints the session instead of erroring out. SecurePairingStore + MinosCoreProtocol gain peerDisplayName get/set so the QR's host_display_name survives reboots; AppShell's runtime row and profile header read it via a peerDisplayNameProvider, falling back to "Agent Runtime" when missing. ForgetPeer wipes it. Test fixtures updated for the new sendToThread path and for the peer display name surface.
Hash bump for ActiveSessionController (sendToThread) and PairingController source changes from the previous commit.
Output of `codex app-server generate-json-schema --out ./schemas`. Used as the source of truth for the upcoming `minos-codex-protocol` typed crate (see docs/superpowers/specs/codex-typed-protocol-design.md).
Spec for the upcoming `minos-codex-protocol` crate: vendored typify codegen of the JSON schemas in `schemas/`, hand-written `ClientRequest` / `ClientNotification` traits, and post-processor-generated union enums (`ServerRequest`, `ServerNotification`) for typed inbound dispatch. Drives a follow-up refactor of `minos-agent-runtime`: - `codex_client.rs` gains `call_typed` / `notify_typed`. - `runtime.rs` migrates 6 hand-rolled JSON-RPC sites to typed; deletes the `thread_id_from_response` heuristic. - `approvals.rs` is rewritten against the typed `ServerRequest` enum, fixing two latent bugs surfaced while authoring this spec: v2 approval method-name drift and non-conformant `decision: "rejected"` reply payloads (each approval response has its own decision schema). Supersedes §10.1 (wire format) and §6.4 (approval method names) of `codex-app-server-integration-design.md`.
Detailed task-by-task plan for building `minos-codex-protocol` and migrating `minos-agent-runtime` onto it. Phases A (standalone crate), B (runtime migration), C (ADR + cross-references), D (verification), each with TDD-style steps, exact file paths, and ready-to-paste code snippets. Spec: docs/superpowers/specs/codex-typed-protocol-design.md
New crate `minos-codex-protocol`: vendored typify codegen of the JSON schemas in /schemas, hand-written `ClientRequest` / `ClientNotification` traits, and post-processor-generated `ServerRequest` / `ServerNotification` tagged enums for typed inbound dispatch. Driven by `cargo xtask gen-codex-protocol`. The crate is independent (depends only on serde/serde_json). No `minos-agent-runtime` changes in this commit; the migration follows in the next refactor commit. Spec: docs/superpowers/specs/codex-typed-protocol-design.md
CodexClient gains `call_typed` / `notify_typed`. runtime.rs migrates 5
call sites + 1 notification site off `serde_json::json!` onto typed
params from minos-codex-protocol; deletes the `thread_id_from_response`
heuristic, the `initialize_params` helper, and `thread_id_request_params`
since typed responses make them unnecessary.
approvals.rs is rewritten against the typed `ServerRequest` enum. Two
latent bugs fixed in the rewrite:
- v2 approval method names are `item/commandExecution/requestApproval` /
`item/fileChange/requestApproval` / `item/permissions/requestApproval`
(namespaced); the old hand-maintained `APPROVAL_METHODS` string list
used the v1 names, so v2 prompts silently fell through to "warn,
don't reply".
- The auto-reject payload was `{"decision": "rejected"}` for all five
approval shapes, but no schema accepts that string. The typed rewrite
picks `Denied` (v1 ReviewDecision) / `Decline` (v2 CommandExecution +
FileChange) / empty `GrantedPermissionProfile` (Permissions, which
has no `decision` field at all) per variant.
Spec: docs/superpowers/specs/codex-typed-protocol-design.md
ADR records the typed-codegen choice and rejected alternatives. Old `codex-app-server-integration-design.md` gains pointer notes in §4.5 and §6.4 directing the reader to `codex-typed-protocol-design.md` for the current wire format and approval handler shapes; original prose preserved as historical context. (ADR numbered 0019 since 0011 was already taken by `0011-broker-envelope-protocol.md`.)
Pre-deployment: replaces device-keyed pairings with account-keyed account_mac_pairings (migration 0012, next commit) per ADR-0020. Workspace tests will fail until Phase E lands (existing pairings.rs still queries dropped table). check-all skipped intentionally; full gate restored after Phase E commit "mint Mac secret only; insert account_mac_pairings".
New pair model is (mac_device_id, mobile_account_id); paired_via_device_id records the mobile device that performed the scan for audit only. check-all still skipped at this commit — see B1 commit message.
…ptional Per ADR-0020 server-centric routing. Workspace consumers (backend, mobile, daemon) intentionally broken until Phase D-K migrate them; check-all skipped for this transitional commit.
Per ADR-0020 server-centric routing. iOS no longer receives a device secret from POST /v1/pairings — it's bearer-only post-pair. New MeMacsResponse + MacSummary cover GET /v1/me/macs. Workspace consumers (backend, mobile, daemon) intentionally broken until Phase D-K migrate them; check-all skipped for this transitional commit.
Phase D of plan 11. Adds the new (mac_device_id, mobile_account_id)
keyed store module mandated by ADR-0020 and removes the obsolete
device-keyed `pairings` module. Also seeds two test_support helpers
(`insert_account`, `insert_ios_device`) so the new module's tests —
and the upcoming Phase E handler tests — can build a populated pool
without duplicating boilerplate across files.
Adapted from the plan's literal code in three places:
* `mobile_account_id` rides as `&str` / `String` (no `AccountId`
newtype exists in this codebase yet — accounts are opaque UUID
strings everywhere).
* Errors use the existing `BackendError::StoreQuery` /
`StoreDecode` variants (the plan's `BackendError::Storage`
variant doesn't exist).
* `DeviceId` round-trips via `Uuid::parse_str(...).map(DeviceId)`
rather than `String::parse::<DeviceId>()` since `DeviceId` is a
plain `pub struct DeviceId(pub Uuid)` without `FromStr`.
The `exists` query uses `SELECT pair_id` rather than `SELECT 1 AS hit`
because sqlite reports integer literals as untyped, which makes the
`sqlx::query!` macro infer `Option<()>` and fail to compile.
Workspace is intentionally non-building at this commit. Five handlers
(`http/v1/pairing.rs`, `http/v1/threads.rs`, `http/ws_devices.rs`,
`ingest/mod.rs`) still reference the now-deleted
`crate::store::pairings` module. Phase E rewrites those handlers
against `account_mac_pairings::*` and restores the build. Do not push
this commit alone to a shared branch — pair it with Phase E.
Sqlx offline cache: 6 stale `pairings`-table query JSONs deleted, 5
new `account_mac_pairings` query JSONs added. The remaining 11 cache
entries (devices, accounts, tokens, threads, raw_events, refresh_tokens)
are untouched.
iOS row stays at secret_hash NULL per ADR-0020. Pair-table insert moves
from the consume transaction to the handler (post-commit) since the
handler has bearer's account_id; idempotent via ON CONFLICT DO NOTHING.
PairingOutcome drops consumer_secret. Compensation paths now call
account_mac_pairings::delete_pair instead of forget_pair. PairingService
no longer carries forget_pair / clear_secret_if_unpaired_non_host /
normalize_pairing_insert_error — the (mac, account) pair model owns
state and has no per-device idempotency rule to normalise.
Legacy DELETE /v1/pairing route stubbed to 410 Gone (forget_pair was
removed); bearer-authenticated DELETE /v1/pairings/{mac_device_id}
follows in Task E2.
tests/v1_pairing.rs reworked: PairResponse no longer carries
your_device_secret; happy-path test asserts EventKind::Paired.your_device_secret
is Some on the Mac side and that account_mac_pairings::exists is true.
delete_pairing_* tests collapsed to the 410-Gone smoke check (legacy
forget semantics are gone; full e2e returns in Phase M).
Workspace remains in transition (threads, me, ws, ingest, envelope still
use legacy pairings — Phases F-H restore the build); check-all skipped.
Bearer-authenticated; legacy DELETE /v1/pairing stays 410 Gone with
"Use DELETE /v1/pairings/{mac_device_id}" message.
Handler resolves the bearer's account, deletes the (mac, account) row
from account_mac_pairings, and best-effort pushes Event::Unpaired to
the Mac's live session if registered. 404 when the pair does not
exist; 400 on malformed mac_device_id.
E2E test deferred to Phase M (test infrastructure landing there).
check-all still skipped — workspace in transition (threads, me, ws,
ingest, envelope).
Adds DeviceRole as a 3rd arg to classify() to document the ADR-0020 split: iOS rows authenticate via bearer alone (secret_hash stays NULL → UnpairedExisting); Mac rows still use the secret-hash verify branch. Workspace still has 4 errors in threads/me/ws/ingest/envelope — remaining Phase F-H work; check-all skipped.
Threads handlers no longer assert authenticated_with_secret; bearer JWT is the trust root after ADR-0020. require_paired_session is renamed require_authed_session and drops the device-keyed pairings::get_pair lookup — list/read are scoped by the bearer's account_id (one account may be paired to multiple Macs). ingest::history::read_thread now scopes by account_id (not owner device_id) so iOS callers see all paired Macs' threads via single API. v1_threads test helpers switch to account_mac_pairings::insert_pair; the legacy "unpaired returns 401" test is reframed as "missing-bearer returns 401" since pairing is no longer device-keyed. http/v1/auth.rs needed no edits: grep for authenticated_with_secret shows no occurrences in that file. check-all skipped — me.rs, ingest, envelope still pending Phase G/H.
Pre-upgrade auth already routed iOS through UnpairedExisting (NULL secret_hash); the only remaining wire-up was killing the pairings::get_pair call in revalidate_live_session_auth — replaced with Ok(None). Multi-mac presence reconstruction lands in Phase G. The reconnect test that asserted Some(peer) is reframed as returns_none_for_authenticated_mac_reconnect since paired_with is no longer single-valued; module doc updated to match. Workspace still has 3 errors in me.rs, ingest/mod.rs, envelope/mod.rs — remaining Phase G/H work; check-all skipped.
…d_with slot iOS Forward must stamp target_device_id; backend validates against account_mac_pairings::exists for the caller's account. SessionHandle no longer carries a single-peer paired_with slot — that field is removed. Heartbeat collapses to a single PAIRED_TIMEOUT (90s); the legacy notify_live_peer_disconnect / notify_live_peer_connected paths are dropped, and activate_live_session emits Event::Unpaired as the initial frame for every fresh socket. Comprehensive multi-mac presence broadcasts on connect/disconnect are deferred to Phase M. Existing envelope and ws_devices unit tests rewritten against the new contract — no #[ignore]; the rebuild happens against memory_pool + account_mac_pairings::insert_pair fixtures so they still cover happy path, peer-offline-via-no-pair-row, peer-backpressure, and Mac-reply correlation by jsonrpc id. Session registry's Arc strong-count test re-pointed at account_id (same Arc shape) since paired_with is gone. check-all skipped — me.rs (Phase H) still pending.
- render_chat now builds only visible window of lines - RenderCache stored on UiState for the active chat - apply_selection_with_offset handles absolute row mapping - selected_text accepts cache (full-rebuild preserved for now) - tracing spans added to render_chat and render_group_chat Co-Developed-By: opencode <noreply@opencode.ai>
…ments The separator before each item (idx > 0) is part of that item's rendered segment in render_chat. item_starts[idx] must point to the separator line, not the content line. This fixes an off-by-one in line_offset_within_first_segment that caused wrong lines to be displayed when scrolled mid-item.
…aries Adds PromptHistory to InputState so Up/Down at the first/last visual row (after soft-wrapping) browse previously submitted prompts instead of moving the cursor. Esc or Down-past-end restores the in-progress draft. History is recorded on submit for both room and agent inputs. - New PromptHistory struct with previous/next/cancel/record/is_browsing - New pub helpers visual_cursor_row and visual_row_count in input_bar - New InputState::load_history_entry to swap content and reset cursor - Room and agent input Up/Down/Esc handlers updated; submit records history - 21 new unit tests covering PromptHistory and the visual-row helpers
- InputPicker enum replaces agent_picker: None | Agent | Path - PathCandidate and InputPathPickerState types - active_path_range detects path-like tokens (/ or ~/) - list_path_candidates reads directory entries with prefix filter - Tab priority chain: path picker accept → agent picker noop → path token opens picker → cycle focus - Path completion supports ~/ (dirs crate), relative, absolute paths - Directory candidates auto-append / and re-trigger completion
The function returns the index of the last visual row, not a count. Renaming prevents future off-by-one misuse. Also adds a clarifying doc comment on selected_text's cache parameter.
Add workspace_path column (migration 0006), thread it through ProjectRow and store.create_project, and have list_projects return the stored value with a synthetic fallback only when NULL.
Adds list_projects, create_project, list_project_threads, and start_agent_in_project to AgentBackend. start_agent_in_project takes no prompt (the prompt flows via the AgentStartedForPrompt effect later). Implements them on DaemonBackend (RPC), EmbeddedBackend (in-memory), and TestBackend (with with_projects / with_project_threads builders).
…Effect/AppEvent/UiState Introduces the nav type layer (Projects/Sessions/Session/AgentDetail) and extends Action, EffectResult, Effect, AppEvent, and UiState with project-related variants and fields. Handlers, executors, and renderers are added in subsequent phases; temporary no-op match arms keep the crate compiling in the meantime.
… forwarding Replaces Phase 2's temporary no-op arms with real handlers: pure nav handlers (projects/sessions/session levels, dialogs, startup prompt), EffectResult arms that mutate ui state and emit follow-up effects (AgentStartedForPrompt for project sessions), effect executors that spawn backend calls, and ensure_project_session_visible bridging project sessions to the legacy thread list for rendering.
Adds per-nav-level key intercepts at the top of key_to_mapping (projects/sessions/session/dialogs/startup-prompt) that take precedence over the legacy focus-based mappings. Adds resolve_startup_project which loads projects on init, matches the current workspace to a project, and either enters Sessions level or offers to create a new project. Existing legacy-behavior tests are pinned to NavLevel::Session so they keep exercising the chat path.
render_ui now dispatches by nav_level: Projects and Sessions render the new project shell (list + sidebar + optional input bar), Session and AgentDetail fall through to the legacy chat render. Adds Renderable widgets for project list, session list, and the create-project dialog. Also reconciles agent_detail_visible with nav Uplevel so Esc cleanly closes the detail panel when leaving Session level.
…ture-tui Adds nav_integration tests covering projects navigation, dialog flow, Esc behavior per level, and project-bound session creation. Updates architecture-tui.md with the NavLevel/NavAction layer, the new AgentBackend project methods, project AppEvents, and the nav-level render dispatch.
…tegration tests Final review found that AppEvent::ProjectsLoaded / EffectResult::ProjectsLoaded were fully wired but never emitted (resolve_startup_project loads inline). Removes the dead pathway per latest-only policy. Adds two integration tests covering the cross-phase session-entry flows that were the main coverage gaps: start_new_session_via_input (SubmitSessionInput -> StartAgentInProject -> ProjectSessionStarted -> AgentStartedForPrompt) and open_existing_session_bridges_into_legacy_thread_list (OpenProjectSession -> ensure_project_session_visible -> ui.threads).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces room-first group chat plumbing and chat-store-backed MCP integration for agents.
request_agent_helpandmention_user, including persistence, claiming, completion/failure state, and MCP tool permission policy.Impact
Agents can now read the bound room, ask another Minos agent for help through MCP, or mention the user without escaping the room-first chat model. The TUI remains the dispatcher for those queued commands, so room history and visible agent activity stay coordinated.
Validation
cargo fmt --all --checkgit diff --check HEAD~3..HEADcargo test -p minos-chat-store -p minos-agent-runtime -p minos-tuiKnown Validation Gap
cargo test -p minos-chat-store -p minos-agent-runtime -p minos-tui -p minos-daemoncurrently fails in existingminos-daemonintegration tests because those tests still construct oldBackendStatefields and relay sender types.