Layer Keiki onto the existing comet shell, with Copilot as the only harness - #5
Layer Keiki onto the existing comet shell, with Copilot as the only harness#5devin-ai-integration[bot] wants to merge 31 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Runtime test report — headed run on Linux/X11 (llvmpipe)Ran 1. "Sign in to Keiki" crashes the app (reproduced 2/2)Clicking the new gate button kills the process instantly:
2. Keiki is not the first-run default themeFirst run selects Zeron Light / Zeron Dark, with the purple Zeron accent in the settings chrome — Picking Keiki manually works correctly — blue accent, no purple: 3. View-options menu headings off by one"By agent" is present and is the default organization, but the section splits at crates/ui/src/shell/spaces.rs:684-685 weren't bumped for the new row, so "In one list" renders under SORT and "Created" under SHOW. What passed
Not tested (no credentials)Signed-in agent/conversation mapping, populated |
Runtime test report — signed-in Keiki path (headed Linux/X11, real onkeiki.com account)Signed in end to end against production with a real account (5 agents, 7 conversations). The loopback OAuth rewrite works and the earlier Tokio-reactor panic is gone, but the data path had two blocking bugs. Both are being fixed on this branch. 1. All 7 conversations were silently dropped (timestamp parse)Sign-in succeeds and the 5 agents appear, but every agent is empty and the sidebar stays on "No sessions yet": The API returns data — the parse fails.
2. "Sign out of Keiki" left the Keiki rows behindThe session cleared, but all 5 Keiki agent projects were still listed 20s later and the OAuth loopback flow: all green
Not covered / environment note
|
Runtime delta re-test —
|
| notice capped (top of error) | after scrolling inside the notice |
|---|---|
4–5. Plain-text internal rows and relocated entries
- Inbound reads
Internal turn — Devin, outboundInternal turn; no literal**anywhere. Take over/Blockare now on the main context-menu page (betweenArchiveandCopy), and the
Copysubmenu holds onlyConversation link. Take over →Taken over · 29m remaining→ Hand back
restores the original state.New Keiki agent…is now in the sidebar agents dropdown and opens the dialog with templates
loaded (cancelled — no agent created).
Method notes / caveats
- The failing request for the error test was induced by temporarily blocking onkeiki.com's IP at the
firewall, so the message is a client-sideerror sending request for url (…)rather than a server
4xx — same composer notice path, but worth knowing. The rule was removed and the transcript
refetched fine afterwards. - Regression: the new-session canvas still has no Steer/hint and a live send; returning to a Keiki
conversation restores all three. - 0 panics and 0
Keiki timestamp could not be parsedwarnings in the run log.
Runtime test — Keiki agent settings/delete + MCP scope upgrade,
|
consent requests mcp manage |
row after the grant |
|---|---|
Delete agent, once, named correctly
Delete Keiki agent? quotes "Devin test agent"; after confirming the row vanishes without a refresh and the other five agents remain — confirmed against /api/webapp/agents.
Two things to look at
-
Server accepts a nonexistent model. Saving
model = google/gemini-does-not-existreturned success and persisted (visible in/api/webapp/agents). The desktop sent a correct partial update — this looks like missing platform-side validation. Restored immediately. -
Cosmetic (fix in progress): the
System prompttext overflows its box onto theMax steps/History limitfields.
Coverage caveats
- The invalid assigned line case could not be produced:
GET /api/webapp/linesreturns{"lines":[]}for this org and every agent haslineNumber: null, so theLinepicker only offersNo line. The error path was instead exercised by temporarily blocking onkeiki.com at the firewall, so the message shown is a client-sideerror sending request for url (…)rather than a server 4xx. Rule removed; the next load succeeded. - MCP tool execution in harness runs is not testable on this box (no harness CLI / model credentials).
- 0 panics and 0 timestamp-parse warnings in the run log.
Runtime test — Copilot, clean build of
|
Runtime test — zero-agent composer fix, clean comet
|
Runtime test — desktop Copilot against production
|
| run_id | thread_id | status |
|---|---|---|
dbe7682c-3189-4263-83c4-7a750a49ddba |
6384c41a-… |
completed |
744cef54-4a6e-4805-a26a-c01a383ef532 |
6384c41a-… |
completed |
f74d1d17-6083-45e9-82d9-f67d90e3dbfe |
6384c41a-… |
completed |
appendToTranscript works against the merged server
Turn 1 planted a token; turn 2 recalled it (the token is absent from turn 2's request, so only the server-stored transcript can supply it), and copilot_transcripts grew 4 → 6 messages:
Relaunch reopens the same chat and the same server thread
copilotChatId unchanged after relaunch with both turns still rendered; a third turn then reused the same production thread 6384c41a-… (transcript 6 → 8) and recalled the token again — so continuity is server-side, not just local storage:
0 panics, no 401/403 anywhere in the desktop logs. Read-only discipline held: the org's 6 agents are untouched, no approval card was raised (so the decline path was not exercised this round — it was verified locally), nothing sent or steered, no contact messaged.
Two observations, neither a defect in this PR
- The bottom-left account chip still reads
Local onlywhile fully signed in to Keiki (production agents load, three production runs completed) — it tracks comet's own cloud-sync account, not the Keiki session. Cosmetic, but it is the only signed-in indicator a user looks at. - The desktop's
copilotChatIdand the server transcriptthread_idare distinct identifiers, so "same chat reopened" and "same server thread reused" were verified separately rather than inferred from each other.
Summary
Layers Keiki onto comet's existing shell instead of replacing it: Keiki agents become
Spacerows (keiki-agent:<id>), conversations becomeChatrows (keiki-conv:<phone>), and messages map ontoSessionMessageEntry, so the existing sidebar disclosure groups, transcript, composer, pickers and dialogs render Keiki with no new surface. Everything talks to the liveonkeiki.comwebapp API; nothing is stubbed and no action reports success it did not get from the server.Auth is OAuth 2.1 + PKCE with dynamic client registration, refresh and revocation, persisted under
keiki://oauth. GPUI'sregister_url_schemeis unimplemented on Linux, so sign-in uses the RFC 8252 loopback redirect (http://127.0.0.1:<port>/oauth/callback, accepted by onkeiki.com's dynamic registration) and keepskeiki://oauth/callbackfor packaged builds; scheme-registration failure is non-fatal.Keiki and the engine both own rows in the same store, so the reducers are Keiki-scoped:
apply_keiki_snapshotreplaces only Keiki rows and engineapply_chatspreserves them — without that, each poll of either side wiped the other's chats and spaces.Conversation controls (take over / hand back / block / unblock / send / steer) live on the conversation menu, scoped to the selected chat so a menu opened elsewhere can't act on stale state, with stale async results dropped after navigation. Send is gated on a live takeover derived from the server's
expiresAt:A 409 from
/messagesclears the local takeover and surfaces the server's text rather than retrying. Successful send and steer refetch the conversation instead of synthesizing a bubble.Two behaviors worth knowing, both learned from the server and not the docs:
/steeris not a dry run.runSteerTurncallsexecuteTurn({ internal: true })andmarkTurnInternal, so the steering text and the reply are persisted on the real conversation (delivery is skipped; internal mode refuses every messaging tool). The UI says "Real agent turn — recorded on this conversation / Nothing was sent to the contact", and internal rows are labeledInternal turn — <staff>so they don't read as the contact talking.GET /conversations/:phonepagescreated_at ASC LIMIT/OFFSETwith a default of 200, i.e. the oldest page. Long threads therefore never showed their recent messages, regardless of refetching. The client now asks for the newest window:Agent creation goes through the server's template path (
GET /agent-templates→POST /agents), surfaces server validation verbatim, treatsmissing_secretsas a notice rather than a failure, and refreshes and selects the new agent.Conversation rows carry two device-local actions instead of agent management, which was removed:
Pin/Unpinpromotes a conversation to the top of its agent group, andView conversationopens the thread on the dashboard. Pinning storeskeiki-conv:ids inui-settings.jsonand partitions each group while preserving relative order, so it survives a poll replacing every row; the ids self-heal, since a pin whose conversation is gone from a populated snapshot is dropped, while an empty one (signed out, before the first poll) is left alone rather than wiping every pin. The dashboard URL is built from the client's configured base — soKEIKI_API_URLis honoured — with the phone percent-encoded as a path segment andagentIdadded only when the conversation has one; no token or API key ever reaches the URL.The Keiki-tools-over-MCP wiring that an earlier commit on this branch added is gone again, deleted rather than left dormant: the Copilot is taking that job, so
McpServerSpec/RunControls.mcp_servers, the engine-owned holder and itsSetMcpServersRPC, claude's generated--mcp-configfile, codex'smcp_servers.*overrides and ACP'smcpCapabilities.httpprobing all come out, along with themcpscope upgrade and its "Connect Keiki tools…" entry. Sign-in asks formanageonly;discover_oauthstays scope-parameterised because the Copilot rides the same token.Visible copy is Keiki: no Zeron branding, "projects" → "agents", "pull requests" → "conversations", and Keiki Light/Dark (the onkeiki.com blue palette, no purple) is the genuine first-run default. Internal
zeroncrate, binary, daemon and iOS identifiers are deliberately unchanged — renaming them churns packaging and the update/daemon paths.Also fixed along the way: Keiki requests ran on GPUI's executor with no Tokio reactor and panicked on first sign-in (now
gpui_tokio::Tokio::spawn); OAuth task/join failures were misreported as contract violations; production timestamps like2026-08-28 16:58:29.714004+00failed RFC3339 parsing and dropped rows; sign-out left Keiki rows behind; and the signed-out gate set a duplicate hover style, panicking every launch.Verified against a real account in Adam's Organization: sign-in, agent folders, transcripts, send gating, takeover/hand-back, block/unblock, steer, and template creation. Linux sign-in needs a Secret Service — run under
dbus-run-sessionwithgnome-keyringunlocked (captured in the blueprint).Copilot replaces the coding harnesses
Keiki's dashboard Copilot is now a real harness (
HarnessId::Copilot) rather than a bespoke panel, and the seven coding harnesses it replaces are deleted rather than hidden: claude-code, codex, cursor, grok, hermes, pi, opencode, the whole ACP layer, adapter installation, the provider-account plumbing that existed only to log those tools in, and the Harnesses settings page. ~28k lines out. Copilot's runs stream, get steered, get cancelled and raise approval cards exactly like a harness turn, so the engine's run/session/transcript/approval machinery carries it unchanged and nothing is left orphaned behind a deleted backend.crates/copilotis a small client forPOST /api/copilot/chat's AG-UI SSE stream: an incrementalSseDecoder(partial chunks, multi-data:frames, comments, final-frame flush) plus aTurnMapperfolding AG-UI intoAgentEvents — text/reasoning deltas, tool-call chunks accumulated into oneToolCall,TOOL_CALL_RESULT→ToolResult, andRUN_FINISHEDdeciding between continuing, completing and pausing:That middle arm is not a nicety: a model turn that asked for tools ends with its own
RUN_FINISHED, so a delegating turn streams two of them and the answer arrives after the first. Treating the first as terminal ended the turn before the assistant's text and before the interrupt that followed it — the user saw an empty turn and never got the approval card. The finish reason lives atmetadata.tanstack.finishReasonwith a top-level fallback, which is exactly what the server's ownisTerminalFinishreads; an absent reason stays terminal, since the durable log's synthetic replay finish carries none.Three protocol facts the server's persistence layer dictates, none of them documented:
withPersistence'sonConfigdoespatch.messages = config.messages.length > 0 ? config.messages : stored— incoming messages replace the stored thread wholesale. The dashboard can satisfy that because it holds the whole conversation in React state; a desktop client cannot, and the only ModelMessage-shaped transcript lives server-side (GET /threads/:idreads the dashboard's thread store, which this client never writes, andGET /chat?threadId=hands back UI messages). So sms-kit#782 grew anappendToTranscriptflag: a turn posts one user message and the server prepends the thread's own ownership-checked transcript. Without it every turn silently truncated the thread to its newest exchange, and the copilot answered each question as if it were the first.messages: []withparentRunIdandresume— inventing a "continue" turn pollutes what the user sees on the dashboard and replaces the paused run's saved state. The server accepts an empty list only for a request it parses as a real resume.resolveInterrupt({ approved }), and the copilot's approved-program path checksapproval?.approved === true, so a bare{status: "resolved"}approves nothing and the run quietly does nothing. Approve/decline sendresolved+{"approved": true|false};cancelledis reserved for abandoning a card.GET /chat?threadId=asinterrupts.{runId,pending}. The harness hydrates that at the start of every turn and again after a stream that looked complete, raises the cards through the existing input-prompt UI, and resumes againstinterrupts.runId— which is also what unwedges a thread whose pause was missed, since the server refuses new input while an interrupt is pending. Cancellation posts the run id the client minted, not an id read off the stream (the provider's message ids are not durable run ids and 404 on/runs/:id/cancel).Session identity is the thread id, not the run id: the engine feeds
Done.session_idback asRunRequest.resume, which the harness reads as the thread to continue — carrying the run id there would start a fresh empty thread every turn and undo the transcript work above.The sidebar gets a permanent
Copilotrow above the agent groups (muted with "Sign in to Keiki to use Copilot" while signed out). It selects a device-local, project-less chat minted through the normalcreateChatmutation withHarnessId::Copilot, its id persisted ascopilot_chat_idinui-settings.json, so the transcript, composer, stop and approval prompts are the ones already in the app. The bearer reaches the engine overSET_COPILOT_CREDENTIALSon sign-in, keyring restore and every refresh — an access token lives about an hour, so a stale header would break long runs — andSIGN_OUTclears it.Deleting the
HarnessIdvariants can't break devices that already holdclaude-code/codexchats, so unknown ids deserialize toHarnessId::Unknown, with the seven retired ids mapped to&'static strliterals so those rows round-trip byte-identically when a chat is archived or renamed (interning arbitrary strings would leak on every sync). Legacy chats stay visible, archivable and deletable.Server side:
Mail-0/sms-kit#782(merged) lets an OAuth bearer withmanagereach/api/copilotthrough the existingrequireSessionguard — one auth path, a short-lived derived session for bearer-started durable runs, credential-free requests advertising/.well-known/oauth-protected-resource/api/copilot, and theappendToTranscriptflag above. It is live on onkeiki.com, so this branch needs nothing further server-side.Driven end-to-end headed against a local sms-kit carrying #782, with every UI claim checked in Postgres: multi-turn context (the stored transcript grows instead of being overwritten), approval cards raised in the turn that provokes them, approve executing the action and decline leaving it alone, cancellation landing as
status = abortedon the durable run, and relaunch resuming the same thread. Details and frame-level evidence are in a comment on this PR.One thing that pass found, fixed here: the composer was gated on spaces existing, and Keiki agents are the spaces — so a project-less Copilot chat in an org with no agents rendered no input at all, which the Copilot could bring about itself by deleting the last agent. The composer now follows the selection; the no-selection onboarding card is unchanged.
Release Notes:
Link to Devin session: https://app.devin.ai/sessions/45ed77efb9cd4fe39d69b35ee5c9076f
Open in Devin Desktop: https://app.devin.ai/desktop/session/45ed77efb9cd4fe39d69b35ee5c9076f?variant=devin
Requested by: @MrgSub