feat: Dynamic Project Registry (Phase 1) - #2
Closed
marcmantei wants to merge 449 commits into
Closed
Conversation
The supervisor was killing active workers after 300s because the timeout was measured from spawn time, ignoring ongoing activity. Workers running tools and reporting status were still getting killed. Track last_activity_at on WorkerTracker, refreshed by WorkerStatus, ToolStarted, and ToolCompleted events. Timeout now fires only after 10 minutes of inactivity (up from 5 minutes total lifetime).
Prevents a race where run_health_tick() could filter on last_activity_at before pending WorkerStatus/ToolStarted/ToolCompleted events update it.
…r-idle-timeout fix(cortex): use idle time instead of lifetime for worker timeout
docs: sync rig usage/design docs with implementation
The supervisor was still killing idle OpenCode workers after 10 minutes of inactivity because last_activity_at stops updating when a worker enters idle state (waiting for follow-up input). Add is_idle flag to WorkerTracker: - Set when WorkerIdle event is observed - Cleared by track_worker_activity (any WorkerStatus/ToolStarted/ ToolCompleted event means the worker resumed) - Overdue filter skips workers with is_idle=true This complements the last_activity_at fix from spacedriveapp#332 — that fix prevents killing workers that are actively running tools, this fix prevents killing workers that are legitimately waiting for follow-up.
…layed Worker/branch results persisted in the LLM context in three overlapping places with no expiry, causing the channel to reiterate stale results on subsequent unrelated messages: 1. Status block 'Recently Completed' rendered full 500-char summaries into the system prompt on every turn indefinitely 2. History summary injection pushed raw result text as a permanent assistant message after retrigger 3. Channel prompt instruction said 'you must relay' with no awareness of whether results had already been relayed Add a relayed flag to CompletedItem and mark items relayed after a successful retrigger turn. The status block now filters out relayed items so the LLM only sees unrelayed work in 'Recently Completed'. Relayed items expire from the status block after 5 minutes. The LLM's natural-language relay reply remains in conversation history unchanged — the channel retains full context of what was discussed, it just stops being told to relay results it already relayed. Also fixes a pre-existing bug where completed_items pruning (cap at 10) only ran in the BranchResult arm, never for WorkerComplete events.
…encode worker colors Thread the interactive bool from spawn-time through DB persistence, API responses, SSE events, and frontend rendering so the UI can show whether a worker accepts follow-up input. Differentiate opencode workers visually with zinc/neutral colors (vs amber for builtin) across channel timeline, channel cards, and webchat panel.
…erating-relayed-worker-results fix(channel): stop re-summarising worker results that were already relayed
Both OpenCode and builtin interactive workers now emit a WorkerInitialResult after each successful follow-up cycle, not just the initial task. The channel already handles this event by queuing a retrigger, so routed follow-up results now relay to users automatically. Result text is scrubbed through the secrets store before emission, matching the initial-result path.
…-web-embed feat: embed OpenCode web UI in worker detail view
Both OpenCode and builtin interactive workers now write a transcript snapshot to the DB each time they go idle (initial task + every follow-up). If spacebot restarts while a worker is waiting for input, the transcript survives rather than being lost. OpenCode workers get an optional sqlite_pool field; builtin workers already have it via AgentDeps. The snapshot overwrites the blob each time with the complete accumulated history.
On startup, query worker_runs for idle interactive workers and attempt to resume them instead of marking them as failed: - Split reconciliation: only running workers are marked failed; idle workers are left for reconnection - New get_idle_interactive_workers() query loads idle workers with transcript blobs and session metadata - transcript_to_history() converts persisted TranscriptStep[] back into Rig Message history for builtin worker resume - Builtin Worker::resume_interactive() creates a worker with prior history, skips initial task execution, enters follow-up loop directly - OpenCode OpenCodeWorker::resume_interactive() reconnects to existing server+session via get_or_create(), verifies session via get_messages(), repopulates accumulated_parts, enters follow-up loop - resume_idle_worker_into_state() orchestrates resumption into a ChannelState (handles both builtin and opencode worker types) - Startup code in main.rs queries idle workers, groups by channel_id, pre-creates channels with outbound routing, and resumes each worker - Workers that fail to resume are marked failed via fail_idle_worker()
…limit OpenCode workers can no longer be spawned as non-interactive (fire-and-forget). The spawn_worker tool forces interactive=true for opencode, and spawn_opencode_worker_from_state rejects interactive=false as defense-in-depth. Adds directory claim/release tracking to OpenCodeServerPool so only one opencode worker can be active per directory at a time. The directory is claimed before the worker starts and released after it completes (success or failure).
Six fixes from review bot findings: 1. Rebuild SpacebotHook with the correct existing worker ID in Worker::resume_interactive() — Self::build() creates a hook with a fresh random ProcessId, but the resumed worker needs events published under the original ID that matches the DB row and state tracking. 2. Load restored transcript into `history` (not `compacted_history`) so the LLM actually sees prior conversation context on the first post-restart follow-up call. 3. COALESCE(tool_calls, 0) in get_idle_interactive_workers() query to prevent sqlx::FromRow failure on rows where tool_calls is NULL. 4. Replace .ok() with proper error logging on fail_idle_worker() calls in main.rs startup — these are SQL errors, not dropped channel sends. 5. Persist recovered transcript before emitting idle in the OpenCode resume path, so a second crash doesn't lose the rebuilt state. 6. Make persist_transcript() and persist_transcript_snapshot() async and await them directly instead of fire-and-forget tokio::spawn. Ensures "idle implies persisted" and prevents out-of-order writes where an older snapshot could overwrite a newer one.
Previously, when an idle worker couldn't be reconnected on startup (e.g. OpenCode server died with kill_on_drop, session expired), the code called fail_idle_worker() which transitioned the row to 'failed'. Now the worker stays as 'idle' in the DB with its transcript preserved. The resume attempt is best-effort — if it fails, the worker is effectively dead but its history remains available for inspection in the UI. Only dangling 'running' workers (from unclean shutdown) get marked as failed via reconcile_running_workers_for_agent().
…el after restart Two bugs preventing idle worker visibility after restart: 1. opencode_session_id was never persisted to the DB. The OpenCodeSessionCreated event is filtered out by event_is_for_channel() (returns false for that variant), so the channel handler's log_opencode_metadata() call was unreachable dead code. Fix: persist session metadata directly from the OpenCode worker's run() method via sqlite_pool, bypassing the event system entirely. 2. When resume fails (e.g. session expired because server was killed on shutdown), the channel's status block had no knowledge of the idle worker. Fix: on resume failure, still register the worker into the status block as 'idle (session expired)' so it appears in the channel's UI and the LLM knows about it.
Add overflow-x-hidden to messages scroll container, min-w-0 overflow-hidden to user message bubble, and break-all with whitespace-pre-wrap to message text to ensure long unbreakable strings wrap correctly.
Fixes zombie cycle where OpenCode workers with expired sessions were left as 'idle' in the DB on every restart, reappearing in the channel status block as 'idle (session expired)' and showing as 'running' in the workers tab with no way to dismiss them. - Retire irrecoverable idle workers as 'done' (not 'failed') on startup since the worker completed its work — only the follow-up session expired - Pre-filter OpenCode workers without session metadata before creating the channel (these can never resume due to kill_on_drop) - Stop registering expired workers in the channel status block - Add channel_id to OpenCodeSessionCreated event so it routes through event_is_for_channel() properly, removing the persist_opencode_metadata workaround (log_opencode_metadata in channel.rs is no longer dead code) - Add TranscriptStep::UserText variant to preserve user vs assistant text distinction across transcript serialization round-trips - Emit TypingState SSE events from pre-created channel outbound router
Resumed OpenCode workers were losing their original directory because it was never persisted to the DB — resume fell back to workspace_dir, spawning a new server in the wrong directory and ignoring the one-per-directory limit. - Add 'directory' column to worker_runs (new migration) - Persist directory from spawn_opencode_worker_from_state via UPDATE - Read directory from IdleWorkerRow in resume_idle_worker_into_state - Fall back to workspace_dir only when directory is NULL (pre-existing workers without the column)
…worker-resilience fix: persist transcripts on idle + reconnect idle workers on restart
Replace the iframe-based OpenCode embedding with a Shadow DOM approach that mounts the SolidJS SPA directly into a React-owned DOM node. This eliminates all the iframe shims (history patches, TextDecoderStream polyfills, WebKit EventSource workarounds, CSP stripping) while providing proper CSS isolation. Key changes: - Add OpenCodeEmbed React component with Shadow DOM mounting, CSS injection, and SSE directory probing for correct event routing - Add directory field to ProcessEvent::WorkerStarted to fix the race condition where worker directory was set via a separate fire-and-forget UPDATE that could lose to the INSERT - Remove log_worker_directory (superseded by directory in WorkerStarted) - Strip iframe HTML rewriting from opencode_proxy.rs (now a clean transparent reverse proxy for API/SSE traffic only) - Add build script and justfile recipe (just build-opencode-embed) to build the embed bundle from a pinned OpenCode commit - Vendor embed source files in interface/opencode-embed-src/ so the build doesn't depend on a local OpenCode checkout
… handle, signal validation - Swap abort/drain order in cancel_worker_with_reason so the worker is stopped before draining live_worker_transcripts, preventing late events from being lost. - Pass shared live_worker_transcripts Arc into Channel::new so the ChannelControlHandle (cloned during construction) references the same backing map as ApiState. Previously the control handle captured the standalone empty map, causing cortex-driven cancellation to miss transcript data. - Change parse_implicit_signal_shorthand to return Result<Option<…>, String> so malformed shorthands (bad UUID, short phone, empty group) surface structured validation errors instead of falling through to a generic 'no channel found' message.
- Set current_inbound in handle_message_batch() from the last non-system message so the RoutedSender carries correct routing metadata (e.g. Slack thread_ts) instead of stale/empty data. - Replace `let _ =` with `.ok()` on best-effort send_routed calls for Thinking and StopTyping status updates per repo conventions. - Thread slack_thread_ts through add_channel_tools into the cron default delivery target so cron jobs created inside a Slack thread post results back into that thread. Encodes thread_ts as a #thread:<ts> suffix in the broadcast target string, parsed by Slack's broadcast() method.
…ead-conversation-id fix(slack): normalize conversation ID to prevent thread splits
…ool-call-serialization fix: preserve reasoning in assistant tool-call conversion
…8-panic fix: use byte slice in loop_guard to avoid UTF-8 panic on multibyte content
…tance-guard fix: prevent deletion of instance-level skills via agent API
…-panic fix: UTF-8 safe string truncation in browser tool and debug logging
fix(config): override host when deployed to docker
…lidation-docs fix: harden Discord poll handling and refresh docs
# Conflicts: # src/tools/send_message_to_another_channel.rs
Streaming providers (e.g. Kimi) sometimes send content chunks that are just whitespace. The trim().is_empty() guard in collect_openai_text_content and extract_text_content_from_responses_output_item dropped these segments, causing missing spaces in reconstructed output — especially at sentence boundaries and around numbers in lists. Change to is_empty() so only truly empty strings are filtered, preserving legitimate whitespace tokens from streaming deltas.
…-whitespace-drop fix: preserve whitespace-only content segments in streaming responses
Add a SQLite-backed registry that auto-discovers GitHub repositories via `gh repo list`, syncs periodically in the background, and optionally auto-clones new repos. Per-repo config overrides (model, enabled) are preserved across syncs. New modules: - registry/store.rs — RegistryStore CRUD (upsert, list, overrides, link) - registry/sync.rs — discovery via gh CLI, reconciliation, auto-clone - api/registry.rs — 5 REST endpoints for repos, overrides, sync, status Config: [defaults.registry] section with github_owners, clone_base_dir, sync_interval_secs, auto_clone, exclude_patterns. Wired into RuntimeConfig (hot-reloadable), AgentDeps, ApiState, and main agent init loop with background sync task. 9 new tests, all 633 tests pass. Refs: #1 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Send a summary message via MessagingManager when registry sync discovers new repos or archives removed ones. Configurable via `notification_target` in [defaults.registry] (e.g. "telegram:1285309093"). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
The sync loop was spawned before messaging_manager was set on AgentDeps, so notifications were never sent. Now spawns after init, reading the sync status from ApiState. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1. Fix cortex profile 404: default routing model was "anthropic/claude-sonnet-4" (doesn't exist), now uses "anthropic/claude-sonnet-4-20250514". 2. Fix worker outcome reporting: when a webhook-triggered worker completes and Lira relays the result via send_message_to_another_channel (e.g. to Telegram), the retrigger system didn't recognize it as a successful relay because only the reply tool set the replied_flag. Now SendMessageTool also sets the flag on successful delivery. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
16 tasks
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
Implements #1 — Dynamic Project Registry + GitHub App Migration.
Phase 1: Dynamic Project Registry
gh repo listper configured GitHub owner, reconciles against a SQLiteregistry_repostableworker_modelandenabledflags preserved across syncsmarcmantei/liralot-config)MessagingManagerwhennotification_targetis set/api/registry/— list, get, update overrides, trigger sync, statusRegistryConfigfromRuntimeConfig(ArcSwap) each sync iterationPhase 2: GitHub App Migration (infrastructure, no code changes)
marcmanteireposhttps://liralot.tail65530c.ts.netspacebot-gh-tunnel.servicedisabled — no longer neededConfig example
Files added/changed
migrations/20260312000001_registry.sql— new table + unique indexsrc/registry.rs,src/registry/store.rs,src/registry/sync.rs— store + sync + notificationssrc/api/registry.rs— API handlerssrc/config/{types,toml_schema,load,runtime}.rs—RegistryConfigwired through config pipelinesrc/api/{server,state,agents}.rs,src/lib.rs,src/main.rs— integration pointsTest plan
🤖 Generated with Claude Code
via Happy