Repository navigation
Conversation
Bot creation and the supervision tick could both enter start_bot for a new bot before either registered its session. With Codex, the second runtime resumed the thread the first had just created and failed with "thread already has an active writer"; the failure path then cleared the healthy first session, crashing the bot until the next retry. start_bot now claims the bot under the supervisor lock before launching anything, and releases the claim when it returns, so a concurrent caller backs off instead of starting a second runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pair two of the owner's daemons over Tailscale and stand a bot from one in as a linked bot in a project on the other. A linked bot is an ordinary bot row with peer_id set, so addressing, task budgets and history work unchanged; the delivery worker forwards deliveries for it over the /peer link instead of writing to a local session. The receiving daemon stores each forwarded message as if the sender were local: a task is mirrored with the sender's deadline and hop count, a result or cancel closes the mirrored task, and the sender gets a linked bot of its own on first contact so replies route back. Files listed in a result travel with it (from the bot's own directory or the project artifacts only, size-capped) and land in the recipient project's artifacts directory. Pairing is a one-time invite holding a link token bound to the first daemon that presents it. Peers can only list bots, link one, and deliver to bots linked to them; revoking drops the link. `gravityd peer ...` drives pairing and linking until the desktop UI lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs ahilles107/gravity#13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 1 of the chat pane (ahilles107/gravity#13). The daemon now reads a bot's runtime transcript and serves it as turns: what woke the bot (the owner from the composer or the terminal, another bot, a routine, a ruling, a background job), its text, its tool steps with diff counts and images, and Gravity's own bus calls as sent messages, results and decisions. A per-bot reader folds the transcript in incrementally and a watcher pushes chat_turns for loaded chats as they grow. Step detail, images and files are fetched on demand; read_file only reaches the bot's own directory and its project's artifacts. New read-grant requests list_chat, get_chat_step, get_chat_image, list_artifacts and read_file, behind a `chat` capability. Also fixes two bugs the research turned up: - transcript_dir kept `_`, while Claude Code files every non-alphanumeric character as `-`, so bots under such paths had no activity preview. - Routine runs never completed from the transcript on Unix: the Stop hook did not forward Claude Code's payload (so no transcript_path), and the run-id scan did not expect Claude Code's peer-message wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 2 of the chat pane (ahilles107/gravity#13). Opening a bot now shows its conversation as turns read from the daemon's chat model: who woke it, its answer as markdown with highlighted code, tool steps folded into groups that expand to their command, diff and output, step images with a viewer, sent messages, results with their files, and decisions raised. The composer sends through send_user_message and shows a message's delivery state until the bot reads it. The chat follows chat_turns pushes and pages back through older turns. The Terminal and Routines tabs stay (⌘1-3, ⌘L focuses the composer), the side panel gains a Files tab listing the project's artifacts with previews, and a linked bot from a peer shows its bus conversation. A daemon without the chat capability keeps the terminal-first view. Bot markdown keeps the Control Center's policy: links and remote images are inert; images render only from bytes the daemon served. Also fixes list_messages paging, which sent before_id to a daemon reading before_num. Visual baselines for the new stories still need to come from CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3 of the chat pane (ahilles107/gravity#13). Bots get a blocking PermissionRequest hook (not PreToolUse, which fires for every call the bot is already allowed to make). The hook posts the prompt to /hook/permission and waits; the app shows a card above the bot's tabs with Allow once, Allow for this session and Deny (with a reason the bot sees), and the answer goes back as Claude Code's decision. Nothing defaults to yes: - An unanswered prompt is denied when its window closes (permission_timeout_seconds, default 600, below the hook's timeout). - "Allow for this session" sends Claude Code's suggested rules with the destination forced to session; mode changes and persistent rules are dropped, so no settings file is ever written. - The daemon only holds a prompt while an app that shows cards (hello features: permission_cards) is connected with the control grant. Otherwise, or if the hook fails, Claude Code's own terminal prompt appears as before. - Answering is `control`, the same grant as typing into the terminal. New list_permissions / answer_permission requests and permission_request / permission_resolved pushes. Also splits chat/builder.rs (505 lines, over the limit since phase 1) and moves the Unix hook settings out of paths.rs into paths/unix_hooks.rs. The live Claude Code check the issue requires is still to be run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next to Info and Files, a bot's Tasks tab shows its work in four sections: Now (open tasks it was given), Waiting on others (open tasks it delegated, with the peer machine for linked bots), Upcoming (its enabled routines' next runs) and Done (finished, cancelled or expired tasks, opening to the result that closed them). It refetches after bus traffic and routine runs. A new read-grant `list_tasks` request serves a bot's tasks from both ends, newest first, with the request, the result and when it closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 4 of the chat pane (ahilles107/gravity#13). The Codex worker now records every command, file change (with its diff as Claude Code-style structured patch hunks) and MCP call in codex-observations.jsonl as tool_use / tool_result pairs, and marks each turn's end with a turn_duration record, so the chat reads a Codex bot's turns, steps, diffs and bus calls the same way as a Claude bot's. Command, file-change and permission approvals become permission cards: the worker raises a structured SessionEvent::Permission, the supervisor relays it on the internal bus, the approval broker shows the card, and the answer goes back through RuntimeSession::answer_permission as the App Server's accept / acceptForSession / decline (or a turn- or session-scoped permission grant). Whichever answers first, the card or the native CLI, decides; serverRequest/resolved withdraws the card. Questions and elicitations stay in the native CLI. The broker is split into a shared decide step with Claude hook and runtime front ends. The supervisor's session event loop moves to its own file. Also fixes the scheduler's routine-completion listener, which ended for good on the first internal event that was not BotDone, or on a lagged burst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 5 of the chat pane (ahilles107/gravity#13). A linked bot's chat used to show only its bus conversation. Now list_chat, get_chat_step, get_chat_image and read_file for a linked bot are forwarded to the daemon it runs on, which serves them for bots linked to that peer only (read_file stays confined to the bot's own directory and its project's artifacts there). Turns come back with the local linked bot's id. While a peer has a chat open, the daemon running the bot streams its chat_turns as event frames (no req_id, no answer) and the other daemon pushes them to its clients under the linked bot's id. Daemons advertise `peer_chat`; the app shows the full chat for linked bots on such a daemon, with a note saying which machine the bot runs on, and keeps the bus-conversation view otherwise. Permission prompts of a linked bot are still answered on its own machine. The two-daemon test setup moves to tests/common/peers.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 6 of the chat pane (ahilles107/gravity#13), the parts the issue left out of v1: - Decisions in the thread: a decision a bot raised shows its options in the chat. Picking one, or writing an answer, publishes it in one step (publish_decisions with an inline ruling), as "Answer and publish" does in the Control Center; it needs the approve grant. The card links to the Control Center for drafts, holds and the discussion. - Attachments: attach or paste files in the composer. They upload in chunks through a new control-grant write_artifact request into the project's artifacts/uploads/ (16 MB cap, name confined to its last component), and the message lists their paths for the bot. - Slash commands: a message starting with / is typed into the bot's terminal, as if typed there, with a hint under the composer. - Search: ⌘F filters the loaded turns by their text, steps and trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Search now works like find in page: every match in the loaded turns is highlighted in place (CSS custom highlights), the current one apart, and ▲/▼ or Enter / Shift+Enter step between them, scrolling each into view. Folded step groups open while searching so their text is found. Turns are no longer filtered out. - Tasks: requests, results and routine prompts clamped to two lines get a "Show more" / "Show less" when the clamp hides something or a result is waiting, instead of only an ellipsis. - Image thumbnails keep their image's aspect ratio (fixed height, width from the image) instead of a square crop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Tasks list trims each request to 400 characters and each result to 600, so "Show more" could only reveal a cut-off preview. The list now says when it trimmed (request_truncated, result_truncated), a new read-grant get_task request returns one task with its full request and result, and opening a row loads it when the preview was cut. Opened requests and results render as the markdown bots write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mic button in the composer turns speech into text with the operating system's own recognizer, so audio never goes to Gravity or a third party. On macOS, AVAudioEngine feeds the microphone to Apple's Speech framework (objc2-speech / objc2-avf-audio), set to recognize on the device whenever the Mac supports it. Partial transcripts stream into the draft after what was already typed, and the final one stays; pressing the button again stops. The first use asks for Speech Recognition and Microphone access: the app now carries NSSpeechRecognitionUsageDescription and NSMicrophoneUsageDescription (src-tauri/Info.plist), and signed releases get the hardened-runtime audio-input entitlement (Entitlements.plist). The mic is hidden where dictation is unavailable: a plain browser, and Windows until its recognizer (Windows.Media.SpeechRecognition) lands. The composer's attachments move into useAttachments and its buttons into ComposerParts. Notices gain the seven objc2 framework crates, which ship no license file and use the reviewed objc2 LICENSE override like their siblings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full-size image viewer reused the command palette's overlay panel, which is 580px wide, at most 60vh tall and clips its content, so wide screenshots were cropped into a box. Images now open in a full-window lightbox: fitted to the window at their own aspect ratio, or at actual size (click the image) with the stage scrolling. ←/→ and the side buttons page through a step group's images; Esc or the backdrop closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composer's mic was macOS-only. On Windows, SAPI's in-process recognizer with its dictation grammar now feeds the same partial and final events, so audio stays on the PC. WinRT dictation was not used because it requires Windows' online speech service. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation fec6522 changed apps/desktop/src-tauri/Cargo.toml and Cargo.lock without regenerating the notice inventory, so `scripts/notices.py` refused them. It added no crates: the regenerated records and bundles are identical, so only the two input hashes change. The records are kept as committed rather than regenerated here, since a macOS run swaps the marketing build tools' Windows platform packages for macOS ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eleven stories added with the chat pane had no baseline, so the visual suite failed on each: chat composer, code blocks, lightbox, permission cards, tasks panel and turn view. Generated in the pinned Playwright container with --update-snapshots=missing, so the existing 72 baselines are untouched and still match byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After revoke_peer, pairing the same two daemons again failed with "a peer named '<name>' already exists": the revoked row kept its name under the UNIQUE constraint. It also kept the other daemon's id under its own UNIQUE constraint, so even a new name would have left the link unable to bind. Revoking now tombstones the name the way archiving a bot does (`name#<id8>`) and releases the daemon id. Views show the owner's name, not the tombstone. Migration 15 does the same for peers revoked before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linking a project here with one on a peer makes the two one team: every bot that runs on either side is exposed to the peer and stands in on the other side, and stays mirrored while linked. This is the daemon half of the protocol GravitiOS is built against (gravitiOS docs/linked-projects.md). - hello_ok carries `daemon_id`; capabilities add `linked_projects`. - A `project_link` table (migration 16) records each link on both sides; one link per (project, peer), and per (peer, remote project). - Projects gain `links` (peer_id, peer_name, online, remote_project_id, remote_project_name, linked_at), and `project_updated` is pushed when they change. - Control plane: `list_peer_projects` (read), `link_project` and `unlink_project` (control), and `create_bot` with `peer_id`, which creates the bot on the peer and replies with its stand-in here. Errors keep their code across the link: `conflict`, `not_linked`, `not_found`, `unavailable`. - Peer frames: `list_projects`, `link_project`, `unlink_project`, `create_bot`, `update_bot`, `delete_bot`, and the `project_roster` and `project_links` events. A peer acts only on projects linked through it, except `list_projects` and `link_project`; accepted links are logged. - Mirroring: a watcher on the daemon's own pushes sends a linked project's whole roster on any bot change; the receiver creates, updates and archives stand-ins to match. Events on a link are applied in order, and each link-up resends every roster and drops links the other side no longer holds. - A link whose rosters share a bot name is refused, naming the bots. Unlinking from either side, revoking the peer or archiving the project archives the stand-ins on both sides (cancelling their open tasks) and stops the peer reaching the project's bots. - Stand-ins no longer count towards `max_bots_per_project`. - MCP `create_bot` takes `machine`, allowed only when the project is linked through that peer; `update_bot` and `delete_bot` on a bot created there are forwarded, and the peer holds them to the creator rule. The system prompt names the machines a project is linked with. - The owner's `update_bot`, `set_bot_runtime` and `delete_bot` on a stand-in in a linked project are forwarded to its machine, since changing only the stand-in would be undone by the next roster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
protocol.md lists the new requests, `create_bot`'s `peer_id`, the project's `links`, `daemon_id` in hello_ok and the `linked_projects` capability. peer-bots.md gains a "Linked projects" section (what a link means, how linking, mirroring and creating on the other machine work, and the peer frames), notes that revoking frees the peer's name, and adds the schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
|
would be great if you could rebase it. |
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.
What
Links a project on one daemon with a project on a paired peer, so the two act as one team across two machines. This is the daemon side of the protocol the GravitiOS phone app is built against.
What a link means
conflict, naming the clashing bots.max_bots_per_project.Control plane
hello_okcarriesdaemon_id, and capabilities addlinked_projects.links(peer_id,peer_name,online,remote_project_id,remote_project_name,linked_at), andproject_updatedis pushed when links change.list_peer_projects(read), andlink_projectandunlink_project(control).create_botwithpeer_id: creates the bot on the peer and replies with its stand-in here.conflict,not_linked,not_foundorunavailablewhichever daemon refused.update_bot,set_bot_runtimeanddelete_boton a stand-in in a linked project act on the real bot on its machine. Changing only the stand-in would be undone by the next mirror.Bots
create_botgainsmachine, allowed only when the bot's project is linked through that peer.update_botanddelete_boton a bot created there are forwarded, and the peer holds them to the same rule as locally: a bot manages only bots it created.Between daemons
list_projects,link_project,unlink_project,create_bot,update_botanddelete_bot.project_rosterandproject_links.list_projectsandlink_project. Pairing is the owner's consent for either side to propose a link, and each accepted link is logged.Fix: pairing again after a revoke. After
revoke_peer, pairing the same two daemons again failed with "a peer named '' already exists". The revoked row kept its name and the other daemon's id, and both must be unique. Revoking now tombstones the name the way archiving a bot does, and releases the daemon id. Migration 15 does the same for peers revoked before this change; migration 16 addsproject_link.Why
A team that spans a Mac and a Windows PC had to be wired up one bot at a time with
link_peer_bot, and drifted as bots were added or renamed. Linking the project keeps the whole team in step on both machines. It also lets a lead create a Windows-side bot itself, and lets the owner do it from the phone.How tested
crates/gravityd/tests/linked_projects.rsruns two in-process daemons paired over a real peer link. It covers:create_botwithpeer_id, includingnot_linked;create_botwithmachine, plus forwarded update and delete, and the refusal when the project isn't linked;daemon_idand the capability in the handshake.peer_bots.rs;migration.rs;scripts/verify.shpasses, including 368 Rust tests and all 83 visual stories.🤖 Generated with Claude Code