improvement(mothership): mothership v0.9 - #6815
Open
Sg312 wants to merge 106 commits into
Open
Conversation
…ocs, account context Squash of the feat/platform-agent branch (sim side): mounts the Sim docs corpus in the copilot VFS, wires search_docs and retires the legacy docs search tools, and syncs the generated tool catalog and trace contracts for the platform subagent.
# Conflicts: # apps/sim/lib/knowledge/application/documents.ts
Pulls the mothership tool catalog (wait_agents / tail_agent / steer_agent / interrupt_agent), trace spans (chat.async_subagent.*, chat.orchestrate.*), and trace attributes (copilot.async_subagent.*) into the generated TS contracts.
wait_agents / tail_agent / steer_agent / interrupt_agent get natural-language running titles (naming the agent id being waited on, tailed, steered, or stopped) and a Steering→Steered completed-verb rewrite.
A subagent_start whose payload data carries a name (the orchestrator's new name trigger parameter) now labels the agent group with that mission name — the agent-type icon stays. The name flows through the live stream path, the turn model (AgentNode.displayName) and its serialize/rebuild round-trip, and persisted transcripts (PersistedContentBlock.name), so reloads keep the label.
The dispatch-time subagent_start fires before the trigger args (and therefore the name parameter) have streamed; the phase-3 start re-announces the lane with the name. The block builder was dropping that duplicate wholesale, losing the name on streaming providers — now it backfills subagentName onto the existing block instead. (The home turn-model path already reconciled this case.)
The wait_agents title ignored the mode argument, so an any-mode wait over three agents read 'Waiting for 3 agents' while the model narrated waiting for the first — contradicting the transcript.
Subagents now narrate their work through <intent>3-5 words</intent> tags (a fleet-wide prompt protocol on the mothership side). The turn model streams each subagent's text through a split-safe tag parser: complete tags update the agent's currentIntent and disappear from the prose, tags split across deltas are carried until their close arrives, and a tag that never closes flushes back as plain text. The agent card renders as one line — display name (or agent label) plus the latest intent, replaced inline as the agent shifts gears — and never auto-expands; expanding to the full tool log is a deliberate click. Only an outstanding permission prompt or a browser hand-back forces a group open. Intents persist on the subagent block (and through the legacy persisted- message paths) so reloads keep the last status, and a renamed reinvocation now takes the latest name instead of pinning the first.
POST /api/copilot/tools/execute (INTERNAL_API_SECRET, Go→Sim) runs one sim-server tool through the same server tool router the resume driver uses and returns the result synchronously — no checkpoint. This is what lets background (async) subagents write files/tables/knowledge, and lets the main lane keep streaming (instead of checkpoint-pausing and killing every background run) while async agents are live.
Files/tables created through the internal execute route now register on the chat's resources exactly like the resume driver's executions — the route runs the same handleResourceSideEffects pass (persistence only; an out-of-band route has no live event sink, so mid-turn chip pushes are a follow-up).
…redential groups Three more read-only files, all lazily loaded — the paths appear in the key view so glob discovers them, but no query runs until a read — and all gated by registration, so an unpermitted viewer's file simply does not exist: - workspaces.json (org members): the org's full workspace map with the viewer's access flag and fork parentage — account/workspaces.json only ever showed what the viewer can reach. Inaccessible workspaces stay nameable, not readable. - permission-groups.json (org admins): every group with member count, targeted workspaces, and the restrictions its config activates. access-control.json remains the per-viewer binding. The queries are lifted into lib/permission-groups/queries.ts because their only prior home was inline drizzle in the route handlers, which the VFS cannot import. - credential-groups.json (entitlement-gated): per-option configuration readiness and enrollment progress — the two facts that decide whether a credential_group workflow will do anything at runtime. The note teaches the contract that bit the audit: an active group with zero completed enrollments yields an empty loop, not an error. Enrollee emails are workspace-admin-only, matching the settings page; counts come from the first enrollment page and say so when truncated. The README documents each file only when mounted for this viewer.
The field case: 'Begin Assessment' submits a form. The navigation tears the origin document down while the press completes, so everything after dispatch — the CDP call's own completion, the synthetic dispatch's return value, the postcondition reads — fails against a destroyed context, and a maximally successful click was reported 'Failed clicking element'. The agent's own follow-up investigation in the transcript diagnosed exactly this. navigationRescue detects it at the driver level, where the navigation epoch and URL survive the renderer teardown: when the page provably navigated since dispatch began, a dispatch-path failure becomes a success carrying navigatedDuringDispatch and a note explaining why no postconditions exist. Applied to all four click dispatch paths (unframed CDP, framed native, synthetic in-page, click_at). The soft path had the same blindness: with the after-state unreadable, urlChanged computed false and a navigating click reported 'no observable change'. navigatedByDriver now folds into navigated/effectObserved, which also keeps the new-dialog obstruction check meaningful on real navigations.
The punch-through: a modal opens, the native view hides behind a painted snapshot, and the renderer records applied: true. Then one heartbeat commit is skipped — renderer jank past the 2.5s bounds-lease TTL is enough — and main expires the lease, resetting panelOccluded on its side. The next heartbeat finds the modal marker still present and calls setDesired(true), but the lease's dedupe sees applied === desired and sends nothing; the bounds commit that follows lays out an unoccluded native view above the open modal, and no later event ever re-hides it. The comment on this branch already claimed it 'reasserts the lease' — the dedupe made that claim false exactly when the lease had been lost. While the occlusion marker is present, each heartbeat now drops the applied belief (assumeRevealed) before setDesired, so the reassert is a real, forced, idempotent hide IPC — one per second while a modal covers the browser — and any main-side lease loss self-heals within a heartbeat.
A field run burned five snapshot->click cycles on a STATIC landing page, every one refused with the same sentence — 'the page changed since the last snapshot' — and the agent reasonably concluded the page was regenerating its DOM. It was not; the resolver was refusing, and the message could not say why. Five distinct conditions produced that one string: an id missing from the registry, a connected node whose identity drifted, the view-changed adoption gate, no confident replacement, and a replacement tie. The resolver now stamps the reason (with the drifted node's current identity, or the from->to paths for the view gate) and every stale producer carries it into the driver message. Same pattern as not-editable: a refusal that names its cause costs one round; an opaque one costs a loop and a wrong theory in the bug report.
…y paints The blank-page report: a navigation completes while the view is hidden, the page 'finishes loading', and the panel shows white until the user re-navigates by hand. invalidate() on reveal was already there but recomposites the LAST frame — and the last frame is blank, because background throttling suspended the rAF the page's SPA paints its first frame from. The reveal now pulses throttling off (forcing the renderer to produce a real frame), invalidates, and hands the policy back to the session a second later through reassertTabThrottling, which preserves the automation-tab exemption.
Camera and microphone: 'media' joins the agent partition's allowlist, but every grant is gated on the macOS grant first — asked via systemPreferences.askForMediaAccess so the system prompt appears on first use, and answered from getMediaAccessStatus on checks — so System Settings stays the real authority and a page can never hold a grant the OS refused. Granting site permission without the OS grant produced the misleading NotReadableError Google Meet showed. Packaging gains the camera entitlement and usage string (macOS kills the process on prompt without one) and the mic string now covers meetings. Passkeys: WebAuthn itself is Chromium-native and nothing in our handlers blocks it — USB security keys need no permission at all. The hybrid transport (passkey on a nearby phone via QR) rides Bluetooth, which signed builds silently lacked: the bluetooth entitlement and usage string enable it. iCloud-Keychain platform passkeys remain outside what an entitlement here can grant — Apple restricts that to approved browsers.
…y_file_edit The model batches prepare_file_edit and apply_file_edit into one round and the Go loop runs same-round tools concurrently, so apply could reach the executor before its prepare staged the intent. The instant no-intent error cost a model retry round and flashed 'Failed creating …' on the shared file row before the retry succeeded. The apply handler now polls briefly (10s cap) for the intent; a truly missing prepare still errors at the deadline.
The pdf model for agent pages: the .html file keeps the markdown-shaped source (frontmatter + prose + sim: fences) and every surface renders the docs-styled document on demand — preview panel, /api/files/serve, public shares, and downloads all call the same pure compiler, now shared in lib/workspace-files. The docs chrome is reproduced from the real fumadocs source: the left sidebar's exact pill metrics, the clerk TOC with its animated scroll indicator, divider-style tables; cards and stats left the vocabulary. Table cells and kv values render inline markdown, and sim:workflow/table/knowledge/file links resolve to real workspace routes, bridged out of the sandboxed preview to the app router. Hand-written imitations of rendered output are rejected at apply_file_edit with a steer back to source, and a streaming page hides its source behind the live rendered preview (batched ~2s) the way a generating pdf hides its script.
Baseline reconciliation: route count to the merged union (1125), and the settings-page module-graph baseline re-recorded for the page-compile / artifact-stylesheet statics now reachable via file-utils.server.
Rails were gated at 1100px of iframe width — the chat resource panel never reaches that, so pages rendered single-column there. The rails now stagger the way the docs do on a laptop: >=640px keeps the section sidebar (240px) beside the content, >=1060px restores the full three-column frame with the clerk TOC, and only a truly narrow pane collapses to one column.
…frames, pagination, and images Clicking a TOC or section link (or pressing Enter in the section filter, which clicks one) navigated the sandboxed frame off about:srcdoc in Electron and landed on a cookie-less sign-in page — the shell now intercepts every '#' anchor and scrolls directly. The page chrome gains the docs' exact theme toggle (emcn Sun/Moon, 30px rounded-lg, top right), framed code blocks with language label and copy button, and footer previous/next cards from prev/next frontmatter. Workspace images () compile to /api/files/view and the preview host inlines them as blob: URLs so the cookie-less frame can render them; sim:accordion joins the vocabulary as the faq component with title keys.
The left rail was a fixed 240/300px column, so it only earned its place once the pane was wide. fit-content caps at the docs width but shrinks to the longest section title (150px floor for the pills), and the two-column tier now starts at 560px instead of 640.
Same move as the section sidebar: the TOC column fits its longest link (capped at the docs' 268px, 150px floor), so the full three-column frame starts at 860px of pane width instead of 1060.
The preview bootstrap cancelled every non-anchor click, so an external link (the Sim docs, a vendor page) did nothing. External http(s) links now compile with target=_blank rel=noopener for the standalone and share surfaces, and the sandboxed preview bridges the click to the host, which window.opens a new tab — same channel the workspace deep links use.
The record's contentType is stamped text/x-sim-page when apply_file_edit detects page source — the file stays .html to the user (serving and downloads still emit text/html), but every surface now knows what the file holds before content loads. The viewer forces the rendered view for these files at every moment: the first streamed chunk (whose frontmatter is still partial) no longer flashes raw source, the gaps between an agent's tool calls no longer flip back to raw HTML, and both toggle surfaces (the Files toolbar and the resource-panel tabs) stop offering a code view for them. Mid-stream compiles run lenient — a fence still being written is malformed by definition, so its skip-notice callout is suppressed until the stream settles.
An explicit contentType on create_empty_file always wins (the skill now declares text/x-sim-page for pages, text/html for bespoke raw pages); with no declaration a copilot-created .html defaults to the page type. The first apply_file_edit still re-confirms from the actual content, and the category map knows the internal mime explicitly instead of falling through to the extension.
A file is a Sim page only when the model declares it at creation or the first written content proves it — never by extension alone.
The invented bordered cards with Previous/Next labels are replaced by the docs' actual footer: the destination name with a 14px emcn chevron on a flex-1 hover pill (rounded-lg, px-3 py-3, --surface-active), next right-justified, and a spacer holding the empty half — verified against apps/docs/components/docs-layout/page-footer.tsx.
The sticky TOC box is overflow-y auto, and the clerk track's absolutely positioned SVGs could tip it a few pixels into overflow — Chromium then painted a full scrollbar beside the rail. Rails now hide their scrollbar chrome entirely (scrollbar-width none + webkit display none), matching how the docs scroll their sidebar and TOC.
A workspace-file link in a page now navigates exactly as one tagged in a
.md does: an in-app SPA push to /workspace/{ws}/files/{id} (the Files
page with the file open). The fullscreen /view route stays reserved for
the standalone surface; image refs keep the /api/files/view byte route.
The blob substitution ran on the raw source, where the compiled /api/files/view src it looks for does not exist yet — so the sandboxed cookie-less frame fetched every image itself and got 401s (broken image icons). The substitution now runs on the built document, covering compiled pages, legacy stored-compiled pages, and bespoke HTML alike.
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
Mothership v0.9
Companion: https://github.com/simstudioai/mothership/pull/440
Type of Change
Testing
Manual
Checklist