Skip to content

Commit 1663b4b

Browse files
GiniGini
authored andcommitted
Close active task mutation boundary
1 parent f1fb4c8 commit 1663b4b

4 files changed

Lines changed: 12 additions & 7 deletions

File tree

HANDOVER.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ONEVibe — Agent Handover Document
22

33
> **Date**: 2026-07-17
4-
> **Status**: Local phases 1–3 are implemented and the professional UI has crossed the Zustand and ordinary-collection TanStack Query boundaries. Governed MCP declarations, a tested opt-in MCP capability facade, truthful demo skill status, a GitHub-catalog skill marketplace boundary, feature-gated Better Auth, authenticated owner scoping, and a reviewed Drizzle/Postgres import proof are present. LiteLLM-only enforcement remains mandatory. The running application is still SQLite-backed; protected Claude/provider acceptance, production auth, the Postgres repository/runtime switch, deployment, cloud sandbox attestation, MCP secret brokering/health, and the final active-task Query mutation boundary remain open.
4+
> **Status**: Local phases 1–3 are implemented and the professional UI has crossed the Zustand and ordinary-collection TanStack Query boundaries. Governed MCP declarations, a tested opt-in MCP capability facade, truthful demo skill status, a GitHub-catalog skill marketplace boundary, feature-gated Better Auth, authenticated owner scoping, and a reviewed Drizzle/Postgres import proof are present. LiteLLM-only enforcement remains mandatory. The running application is still SQLite-backed; protected Claude/provider acceptance, production auth, the Postgres repository/runtime switch, deployment, cloud sandbox attestation, and MCP secret brokering/health remain open.
55
> **For**: The next agent (or human) picking this up cold.
66
> **Read this entire document before touching any code.**
77
@@ -53,7 +53,7 @@ The abstraction that enforces this: `server/runtime-adapter.ts` — the `Runtime
5353
3. **The running app is not Postgres-backed** — local user scoping is proven, and the Drizzle schema/import/restart proof exists; the TaskStore repository adapter, idempotent application-level proof, and `DATABASE_URL` runtime switch remain Phase 4 work
5454
4. **No managed deploy path** — a non-root Docker image and local Compose smoke path now exist, but Railway/Fly configuration, secrets, auth, and production operations remain open
5555
5. **No production sandbox attestation** — local host and development-provider paths must not be described as microVM isolation or default-deny egress
56-
6. **The active task remains an intentional state boundary** — durable SSE replay and the active snapshot are still owned by `useTask`; remaining active-task mutations must not create a second client authority
56+
6. **The active task remains an intentional state boundary** — durable SSE replay and the active snapshot are still owned by `useTask`; active-task mutations use Query mutation lifecycle/pending state and reconcile server-derived caches without creating a second client authority
5757
7. **Remaining extension/release gaps** — protected provider-backed marketplace materialization, production MCP secret/health controls, dependency advisory resolution, and browser evidence remain open in `TODO.md`
5858

5959
### How to run it locally

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Reference: `plan/04-cloud-infrastructure.md`
7979
Reference: `plan/05-ui-overhaul.md`
8080

8181
- [x] **P5-01** Migrate state management — `useUiStore`, `useComposerStore`, and `useSessionStore` own navigation/inspector, composer, and auth state; all ordinary server-backed collections are now Query-owned. Active task snapshots remain intentionally isolated in `useTask` because they are durable SSE projections, not generic client state.
82-
- [ ] **P5-02** Adopt TanStack Query — QueryClient is mounted at the app root; Skills, runtime readiness, MCP declarations, Projects, Schedules, Library, paginated Conversations, and task inventory now use cached queries, and active-task stop/retry/follow-up/branch/share/guidance/project/tag actions now use mutation hooks. The active task/SSE snapshot remains intentionally on `useTask`; close only after mutation-level pending/error UX and active-task cache reconciliation are covered without duplicating server authority.
82+
- [x] **P5-02** Adopt TanStack Query — QueryClient is mounted at the app root; Skills, runtime readiness, MCP declarations, Projects, Schedules, Library, paginated Conversations, and task inventory use cached queries, and active-task stop/retry/follow-up/branch/share/guidance/project/tag actions use mutation hooks with explicit pending controls and toast errors. The active task/SSE snapshot remains intentionally on `useTask`; successful mutations reconcile server-derived caches or refresh that snapshot without duplicating stream authority.
8383
- [x] **P5-03** Add toast system — Sonner is mounted globally; task/project/schedule/MCP/share/runtime failures now surface as user-visible notifications, and the duplicate schedule confirmation was removed. Deliberately remains a client error-surface slice, not a replacement for server evidence.
8484
- [x] **P5-04** Fix all dead controls — removed the two decorative Settings controls, made workspace refresh reload task files, and replaced the hardcoded skills count with the live catalog size
8585
- [x] **P5-05** Add conversation search UI — sidebar search debounces against the server-backed `/api/conversations?q=...` contract and retains the loaded-history fallback for short queries

docs/IMPLEMENTATION-LOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
- Integrated the facade into non-chat Claude Agent SDK tasks behind `ONEVIBE_MCP_FACADE_ENABLED=true`. When enabled, raw configured MCP servers are not exposed to the SDK; only `search_capabilities` and `execute_capability` are registered. It does not grant approval authority or bypass LiteLLM.
1414
- Added a deterministic child-process fixture test covering search, exact-ID execution, unknown-capability rejection, and abort handling. `npm run lint`, `npm run test` (50 files / 247 tests), `npm run build`, `npm run db:check`, and `npm run check:e2e-harness` pass. Production MCP health, secret brokering, authenticated ownership, and protected provider acceptance remain open.
1515

16+
## 2026-07-17 — active-task Query mutation gate
17+
18+
- Closed the remaining local P5-02 boundary. Active task stop/retry/follow-up/branch/share/guidance/project/tag actions already use TanStack Query mutation hooks; this pass makes share requests and queued-guidance removal visibly pending, keeps the composer disabled during active mutation requests, and preserves toast-based mutation errors.
19+
- `useTask` remains the sole server-authoritative SSE/replay snapshot. Successful mutations either refresh that snapshot or reconcile server-derived task/conversation/library caches; no second browser-owned transcript store was introduced. `npm run lint`, `npm run test` (50 files / 247 tests), and `npm run build` pass.
20+
1621
## 2026-07-17 — handover evidence reconciliation
1722

1823
- Reconciled the phase checklist against the current implementation and regression evidence: P1-01 backend-offline recovery, P1-08 permanent simulation disclosure, and P2-07 durable guidance queueing are complete and now marked as such in `TODO.md`.

src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Bell, ChevronDown, CodeXml, Link2, Menu, Monitor, PanelLeftClose, Paperclip, RotateCcw, Share2, ShieldCheck, Sparkles, Square, TriangleAlert, X } from 'lucide-react'
1+
import { Bell, ChevronDown, CodeXml, Link2, LoaderCircle, Menu, Monitor, PanelLeftClose, Paperclip, RotateCcw, Share2, ShieldCheck, Sparkles, Square, TriangleAlert, X } from 'lucide-react'
22
import { AnimatePresence, motion } from 'framer-motion'
33
import { useInfiniteQuery, useMutation, useQuery, useQueryClient, type InfiniteData } from '@tanstack/react-query'
44
import { lazy, Suspense, useCallback, useEffect } from 'react'
@@ -388,7 +388,7 @@ export default function App() {
388388
{backendOffline && <div className="backend-offline-banner" role="alert"><div><TriangleAlert size={15} /><span><strong>Backend offline</strong><small>Run <code>npm run dev</code> in the ONEVibe project root to connect the workspace.</small></span></div><button type="button" onClick={() => void retryBackend()} disabled={retryingBackend}>{retryingBackend ? 'Checking…' : 'Retry'}</button></div>}
389389
<header className="topbar">
390390
<div className="topbar-left"><button className="icon-button" type="button" aria-label={sidebarOpen ? 'Collapse sidebar' : 'Open sidebar'} onClick={() => setSidebarOpen((value) => !value)}>{sidebarOpen ? <PanelLeftClose size={17} /> : <Menu size={17} />}</button><button type="button" className="model-selector"><Sparkles size={14} /> ONEVibe 0.1 <ChevronDown size={13} /></button></div>
391-
<div className="topbar-right"><span className="trust-chip" title="OpenVTC protected · External approvals enabled"><ShieldCheck size={13} /> OpenVTC</span><span className={`connection ${connected ? 'online' : ''}`}><i />{connected ? 'Live' : 'Local'}</span><ThemeToggle /><div className="notification-wrap"><button className="icon-button" type="button" aria-label="Notifications" aria-expanded={notificationsOpen} onClick={() => setNotificationsOpen((value) => !value)}><Bell size={16} />{notifications.length > 0 && <i className="notification-count">{notifications.length}</i>}</button>{notificationsOpen && <motion.div className="notification-panel" initial={{ opacity: 0, y: -5, scale: .98 }} animate={{ opacity: 1, y: 0, scale: 1 }}><header><strong>Activity</strong><span>{notifications.length ? `${notifications.length} needs attention` : 'All clear'}</span></header>{notifications.length ? notifications.map((item) => <button key={item.id} className={item.tone} onClick={() => { setNotificationsOpen(false); navigateToTask(item.task.id) }}><span>{item.tone === 'failure' ? <TriangleAlert size={14} /> : item.tone === 'approval' ? <ShieldCheck size={14} /> : <Sparkles size={14} />}</span><div><strong>{item.label}</strong><small>{item.task.title} · {item.detail}</small></div></button>) : <p>No approvals, queued guidance, or failed tasks.</p>}</motion.div>}</div><button className="share-button" disabled={!snapshot} onClick={() => { if (!snapshot) return; if (snapshot.share) window.open(`/share/${snapshot.share.id}`, '_blank'); else void shareCurrentTask() }}><Share2 size={14} /> {snapshot?.share ? 'Open share' : snapshot?.approval?.action === 'share_artifact' && snapshot.approval.state === 'pending' ? 'Approval pending' : 'Share'}</button><a className="github-button" href="https://github.com/one-computer" target="_blank" rel="noreferrer"><CodeXml size={15} /> GitHub</a></div>
391+
<div className="topbar-right"><span className="trust-chip" title="OpenVTC protected · External approvals enabled"><ShieldCheck size={13} /> OpenVTC</span><span className={`connection ${connected ? 'online' : ''}`}><i />{connected ? 'Live' : 'Local'}</span><ThemeToggle /><div className="notification-wrap"><button className="icon-button" type="button" aria-label="Notifications" aria-expanded={notificationsOpen} onClick={() => setNotificationsOpen((value) => !value)}><Bell size={16} />{notifications.length > 0 && <i className="notification-count">{notifications.length}</i>}</button>{notificationsOpen && <motion.div className="notification-panel" initial={{ opacity: 0, y: -5, scale: .98 }} animate={{ opacity: 1, y: 0, scale: 1 }}><header><strong>Activity</strong><span>{notifications.length ? `${notifications.length} needs attention` : 'All clear'}</span></header>{notifications.length ? notifications.map((item) => <button key={item.id} className={item.tone} onClick={() => { setNotificationsOpen(false); navigateToTask(item.task.id) }}><span>{item.tone === 'failure' ? <TriangleAlert size={14} /> : item.tone === 'approval' ? <ShieldCheck size={14} /> : <Sparkles size={14} />}</span><div><strong>{item.label}</strong><small>{item.task.title} · {item.detail}</small></div></button>) : <p>No approvals, queued guidance, or failed tasks.</p>}</motion.div>}</div><button className="share-button" disabled={!snapshot || shareMutation.isPending} onClick={() => { if (!snapshot) return; if (snapshot.share) window.open(`/share/${snapshot.share.id}`, '_blank'); else void shareCurrentTask() }}><Share2 size={14} /> {snapshot?.share ? 'Open share' : snapshot?.approval?.action === 'share_artifact' && snapshot.approval.state === 'pending' ? 'Approval pending' : shareMutation.isPending ? 'Requesting…' : 'Share'}</button><a className="github-button" href="https://github.com/one-computer" target="_blank" rel="noreferrer"><CodeXml size={15} /> GitHub</a></div>
392392
</header>
393393

394394
<AnimatePresence mode="wait">
@@ -413,8 +413,8 @@ export default function App() {
413413
{error && <div className="stream-warning"><span>{error}</span><button type="button" onClick={retryConnection}>Retry connection</button></div>}
414414
{snapshot.provider === 'demo' && <div className="demo-mode-banner" role="status"><div><Sparkles size={14} /><span><strong>Simulation only</strong><small>No model call is made in this task. Use a configured LiteLLM-backed runtime for real provider execution.</small></span></div>{preferredProvider !== 'demo' && <button type="button" onClick={() => navigateToTask(null)}>Start a new governed task</button>}</div>}
415415
<TaskTimeline task={snapshot} events={snapshot.events} />
416-
<Suspense fallback={<div className="aui-thread-loading">Loading durable conversation…</div>}><AssistantThread task={snapshot} busy={creating || Boolean(snapshot.inputRequest)} onSubmit={continueTask} onSwitchRuntime={(provider) => retryCurrentTask(snapshot.id, provider)} onEditMessage={(messageId, newPrompt) => branchFromMessage(snapshot.id, messageId, newPrompt)} /></Suspense>
417-
{snapshot.queuedGuidance.length > 0 && <section className="guidance-queue"><header><div><ShieldCheck size={13} /><strong>Queued guidance</strong></div><span>Applies after this provider turn</span></header>{snapshot.queuedGuidance.map((guidance, index) => <article key={guidance.id}><div><span>Next {index + 1}</span><p>{guidance.prompt}</p></div><button type="button" onClick={() => void retractQueuedGuidance(snapshot.id, guidance.id)} aria-label={`Remove queued guidance ${index + 1}`} title="Remove before it reaches the provider"><X size={13} /></button></article>)}<footer>Removing a message keeps only cancellation metadata in the evidence ledger.</footer></section>}
416+
<Suspense fallback={<div className="aui-thread-loading">Loading durable conversation…</div>}><AssistantThread task={snapshot} busy={creating || followUpMutation.isPending || branchMutation.isPending || retryMutation.isPending || Boolean(snapshot.inputRequest)} onSubmit={continueTask} onSwitchRuntime={(provider) => retryCurrentTask(snapshot.id, provider)} onEditMessage={(messageId, newPrompt) => branchFromMessage(snapshot.id, messageId, newPrompt)} /></Suspense>
417+
{snapshot.queuedGuidance.length > 0 && <section className="guidance-queue"><header><div><ShieldCheck size={13} /><strong>Queued guidance</strong></div><span>Applies after this provider turn</span></header>{snapshot.queuedGuidance.map((guidance, index) => { const removing = cancelGuidanceMutation.isPending && cancelGuidanceMutation.variables?.guidanceId === guidance.id; return <article key={guidance.id}><div><span>Next {index + 1}</span><p>{guidance.prompt}</p></div><button type="button" disabled={removing} onClick={() => void retractQueuedGuidance(snapshot.id, guidance.id)} aria-label={`Remove queued guidance ${index + 1}`} title={removing ? 'Removing queued guidance…' : 'Remove before it reaches the provider'}>{removing ? <LoaderCircle className="spin" size={13} /> : <X size={13} />}</button></article>})}<footer>Removing a message keeps only cancellation metadata in the evidence ledger.</footer></section>}
418418
</div>
419419
<div className="workspace-pane"><div className="mobile-inspector-bar"><span><Monitor size={13} /> Computer inspector</span><button type="button" onClick={() => setMobileInspectorOpen(false)}>Back to conversation</button></div><Workspace task={snapshot} projects={projects} runtime={runtime} onMoveProject={moveTaskProject} onUpdateTags={setTaskTags} /></div>
420420
</>

0 commit comments

Comments
 (0)