本文へ移動
cccskills
無料GitHub で公開日本語紹介

real-time-sync

エージェントやスクリプトによるデータベース更新を画面へ反映し、SSEとポーリングでクエリを再取得する仕組みを整えるスキルです。

原文原文の説明を見る

How to keep the UI in sync with agent changes via SSE plus polling fallback. Use when wiring query invalidation for new data models, debugging UI not updating, or understanding jitter prevention.

BuilderIO/agent-nativeスター 7,527(GitHub 7,527 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • エージェント更新を画面に反映したいとき
  • 画面が古いデータのままになる原因調査
  • 編集中の入力値を同期で上書きさせたくないとき
  • 頻繁な更新の再取得負荷を抑えたいとき

日本語での紹介

できること

エージェントやスクリプトがデータベースを書き換えたとき、画面側で変更を検知して必要なクエリを再取得する同期方法を整理します。useDbSync()を使い、同一プロセスの変更はSSE(サーバーからのイベント配信)で受け取り、別プロセスやサーバーレス環境の変更はポーリングで補います。useChangeVersionをクエリキーに含める対象を絞り、不要な一括再取得やリクエスト集中を避ける考え方も扱います。自分のタブからの更新を無視して編集中の値が揺れるのを防ぐ方法や、サーバー値をフォームなどのローカル状態へ安全に反映する方法、同期が止まった場合の確認点も説明します。

こんなときに便利

新しいデータモデルを画面に追加するときや、エージェントが更新した内容が表示されない原因を調べるときに役立ちます。複数タブやサーバーレス処理の変更も扱うアプリ、更新頻度が高く再取得のちらつきや負荷が気になる画面にも向いています。

使い方の例

  • 「エージェントが更新したダッシュボードを画面に反映するクエリ構成を教えてください」
  • 「フォームを編集中に同期イベントで入力内容が戻る原因を調べてください」
  • 「更新が多い一覧の再取得を必要な範囲に絞ってください」

注意点

@agent-native/core/client/hooks、React Query、および/_agent-native/eventsと/_agent-native/pollを利用する構成が前提です。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md14.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Real-Time Sync

Rule

The UI stays in sync with agent/script changes through useDbSync(). In-process writes stream over /_agent-native/events first; /_agent-native/poll remains the cross-process/serverless fallback. When the agent writes to the database, the UI detects the change and updates automatically — no manual refresh needed.

Why

The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same-process writes immediately; polling bridges anything SSE cannot see, such as another serverless invocation, cron job, or external script. Every visible write increments a version counter, useDbSync() receives the change, and React Query invalidates the relevant caches. This is what makes database writes feel real-time without relying on aggressive polling.

How It Works

  1. Server increments a version counter on every database write. In-process events stream through the authenticated /_agent-native/events endpoint.

  2. Client listens for sync events and updates per-source change counters:

    import { useDbSync } from "@agent-native/core/client/hooks";
    useDbSync({ queryClient });
    

    For each non-own event, useDbSync bumps a per-source counter (e.g. dashboards, analyses, settings, action) and invalidates a small fixed list of framework-internal prefixes (["action"], ["app-state"], ["__set_url__"], etc.). It does not blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active useActionQuery observers under the ["action"] prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with actionInvalidatePredicate, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.

  3. Templates fold per-source counters into their query keys. This is the pattern that makes "agent writes show up without a manual refresh" reliable:

    import { useChangeVersion } from "@agent-native/core/client/hooks";
    import { useQuery } from "@tanstack/react-query";
    
    const v = useChangeVersion("dashboards");
    const dashboard = useQuery({
      queryKey: ["dashboard", id, v],
      queryFn: () => fetchDashboard(id),
      placeholderData: (prev) => prev, // no flicker on refetch
    });
    

    When the agent writes (update-dashboard action → server emits source: "dashboards"), the counter advances, the queryKey changes, and React Query refetches that one query. The old data stays on screen during the refetch thanks to placeholderData.

    For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.

  4. Fallback polling calls /_agent-native/poll?since=N. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (SSE_FALLBACK_INTERVAL_MS and IDLE_POLL_INTERVAL_MS, both 60 s). A hidden tab polls no faster than every 10 seconds (HIDDEN_POLL_INTERVAL_MS). Polling is the universal serverless fallback: new framework writes are read from the durable sync_events log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.

  5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.

Don't

  • Don't create manual polling loops — useDbSync() handles SSE plus fallback polling

  • Don't create your own fetch-based polling alongside useDbSync — use the onEvent callback for custom handling

  • Don't open your own EventSource to /_agent-native/events. A tab must hold exactly ONE SSE connection no matter how many features listen — extra streams eat the browser's per-origin connection budget and can starve ordinary data fetches (worst on HTTP/1.1 dev servers). Subscribe to the shared transport instead:

    import { subscribeSyncEvents } from "@agent-native/core/client/hooks";
    
    const unsubscribe = subscribeSyncEvents({
      onEvents: (events) => {
        // filter by event.source and handle push-style updates
      },
      // Optional: relax your own fallback cadence while push is healthy.
      onSseStateChange: (connected) => {},
    });
    

    useDbSync and every subscribeSyncEvents subscriber share one EventSource and one fallback poll loop per tab — this is how collaborative documents receive doc updates and cursor/awareness events. Hidden tabs keep that shared transport alive by default: SSE stays connected and active fallback polling relaxes to a 10-second floor (idle polling remains once per minute). Pass pauseWhenHidden: true only for a consumer that explicitly must stop all hidden-tab sync.

Which sources to depend on

Common sources you'll fold into query keys:

SourceBumped by
actionThe agent runner after every successful mutating action tool call
app-stateWrites to application_state (navigation, selections, ephemeral UI state)
settingsWrites to the settings table
dashboardsDashboard CRUD via upsertDashboard / archiveDashboard etc.
analysesAnalysis CRUD
extensionsExtension CRUD
collabYjs collaborative-doc updates
screen-refreshExplicit refresh-screen agent tool call

If a query reads data the agent can mutate via more than one path, depend on multiple sources with useChangeVersions:

const v = useChangeVersions(["dashboards", "action"]);
useQuery({ queryKey: ["dashboard", id, v], ... });

useChangeVersions returns a single integer that advances whenever any of the listed sources advance.

Tuning refetch behavior

To prevent cache thrashing during rapid agent writes, set staleTime on your queries:

useQuery({
  queryKey: ["items"],
  queryFn: fetchItems,
  staleTime: 2000, // don't refetch within 2 seconds
});

Troubleshooting

SymptomCheck
UI not updating after agent writesIs useDbSync called with the correct queryClient? Does the affected query have an active observer?
Poll endpoint not respondingIs /_agent-native/poll accessible? Is the server running?
SSE not connectingIs /_agent-native/events accessible and authenticated? Polling should still keep the UI fresh as fallback.
High CPU / event stormsUse targeted source keys, settle bursty list counters, and avoid broad action invalidation.

Jitter Prevention

When the agent writes to application-state via script helpers (writeAppState, deleteAppState), the write is automatically tagged with requestSource: "agent". This prevents the UI from overwriting active user edits when it receives the change event.

How it works

  1. Agent writes are tagged: the script helpers in @agent-native/core/application-state pass { requestSource: "agent" } to the store.
  2. UI writes are tagged: templates send a per-tab ID via the X-Request-Source header on PUT/DELETE requests to application-state endpoints.
  3. Sync filters: useDbSync() accepts an ignoreSource option. The UI passes its own tab ID so it ignores events from its own writes — but still picks up events from agents, other tabs, and scripts.

Template setup

// app/lib/tab-id.ts — already scaffolded, do not redefine
import { getBrowserTabId } from "@agent-native/core/client/hooks";
export const TAB_ID = getBrowserTabId();

// app/root.tsx
import { TAB_ID } from "@/lib/tab-id";

useDbSync({
  queryClient,
  ignoreSource: TAB_ID,
});

getBrowserTabId() is the id the server resolves tab-scoped application_state and WebMCP calls against — see context-awareness.

The use-navigation-state.ts hook sends the same TAB_ID in the X-Request-Source header when writing navigation state, so the tab that wrote the state does not refetch it.

Why this matters

Without jitter prevention, a cycle occurs: the UI writes state, sync detects the change, the UI refetches and re-renders, potentially overwriting what the user is actively editing. With ignoreSource, the UI only reacts to changes from other sources (agent scripts, other browser tabs, other users).

Action Routes and Live Sync

Actions work with the same sync system. When a mutating action writes to the database, the version counter increments and useDbSync picks up the change. Frontend mutations via useActionMutation automatically invalidate ["action"] query keys on success, triggering refetches of useActionQuery hooks. Client components should call actions through those hooks, not with raw action-route fetches.

For custom apps, the best out-of-the-box path is:

  1. Put read actions in actions/ with defineAction({ http: { method: "GET" } }).
  2. Put write actions in actions/ with the default POST/PUT/DELETE behavior.
  3. Call reads from React with useActionQuery and writes with useActionMutation.

This avoids duplicate /api/* JSON CRUD routes and makes agent-created records show up automatically. Raw useQuery can still work, but it should include useChangeVersions(["action", "<domain-source>"]) in the query key for targeted refreshes.

Auto-emit on mutating actions

The framework emits a change event with source: "action" whenever any non-read-only action runs to completion — whether called via HTTP (/_agent-native/actions/:name) or as an agent tool call. Read-only actions (http: { method: "GET" } or explicit readOnly: true) are skipped.

This means UIs don't need the agent to remember to call refresh-screen after every mutation. A listener like this will refresh after any mutating agent call:

useDbSync({
  queryClient,
  ignoreSource: TAB_ID,
  onEvent: (data) => {
    if (data.requestSource === TAB_ID) return;
    // Invalidate all useActionQuery caches so list-*, get-*, etc. refetch
    queryClient.invalidateQueries({ queryKey: ["action"] });
  },
});

refresh-screen remains available for unusual cases — e.g. the agent mutated data via a path the framework can't see (external system the app mirrors), or the agent wants to pass a scope hint for narrower invalidation.

Keeping Stateful Components In Sync

The useChangeVersion / useActionQuery pattern above keeps the query layer fresh. But components that copy a server value into local React state still go stale on agent edits — refetching the query updates the prop, yet the local copy never re-adopts it. This is a recurring bug.

Never do this for a value the agent can mutate:

// BUG: `title` is captured once and never re-reads the prop.
const [title, setTitle] = useState(props.title);

When the agent renames the record, the query refetches, props.title updates, but the input still shows the stale value until the component remounts.

Derived-state surfaces (form fields, inline editors, popovers): use useReconciledState. It re-adopts the authoritative external value when it changes, except while the user is actively editing that field — so agent mutations show up live without clobbering in-progress typing:

import { useReconciledState } from "@agent-native/core/client/hooks";

// `active` = true while the user is editing this field (focused / dirty).
const [title, setTitle] = useReconciledState(props.title, { active: isEditing });

Collaborative rich-text editors are different — they don't copy a value into useState. They reconcile authoritative SQL content into a shared Y.Doc under an updatedAt gate with lead-client election. See real-time-collab → "Agent edits as a real-time peer editor". Don't reach for useReconciledState for a Yjs-backed editor.

SurfaceKeep it fresh with
React Query readsuseChangeVersion / useActionQuery (above)
Local edit state copied from a server value (inputs, popovers, inline editors)useReconciledState(externalValue, { active })
Collaborative rich-text editor (Yjs)updatedAt-gated reconcile + isReconcileLeadClient — see real-time-collab

Granular server-side merge for non-body fields

For structured documents (slide decks, form builders, design files) where the Yjs body collab would cause LWW conflicts at the container level, pair the change-sync updatedAt bump with a granular server-side merge action that accepts targeted per-item operations (add/patch/delete/reorder). Concurrent edits to different items both survive at the action level; the collab source version bump then propagates the merged state to all open clients. See real-time-collab for the pattern and examples.

Related Skills

  • storing-data — Application-state and settings are data stores that sync through change events
  • context-awareness — Navigation state writes use jitter prevention to avoid overwriting active edits
  • actions — Mutating actions trigger change events
  • client-methods — Route details belong in helpers/hooks, not components
  • self-modifying-code — Agent code edits trigger change events; rapid edits can cause event storms
  • real-time-collab — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync updatedAt bump; also the granular server-side merge pattern for structured data

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

a2a-assets

無料概要あり

複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。

  • スライド用のブランド画像を作りたいとき
  • 既存素材をフィードバックに合わせて直したいとき
  • 動画素材を生成して書き出したいとき
BuilderIO/agent-native7,5272026年10月26日 更新

account-health

無料概要あり

特定の顧客企業について、契約と製品利用の数値を照らし合わせ、根拠や不足情報を明記した健全性レポートをまとめます。

  • 顧客の健全性を確認したいとき
  • 契約席数と実利用を比べたいとき
  • 更新前に利用状況を整理したいとき
BuilderIO/agent-native7,5272026年10月26日 更新

actions

無料概要あり

アプリの操作をエージェントと画面の両方から呼び出せるアクションとして実装し、API連携や画面のデータ取得を一貫した形で追加するスキルです。

  • 新しいアプリ操作を追加したいとき
  • 外部APIと連携したいとき
  • 画面からデータを取得したいとき
BuilderIO/agent-native7,5272026年10月26日 更新

adding-a-feature

無料概要あり

新機能をUI・エージェント用アクション・説明資料・画面状態の4領域でそろえ、利用者とエージェントのどちらからも一貫して使えるようにするチェックリストです。

  • 新機能追加の作業範囲を整理したいとき
  • UIとエージェント操作をそろえたいとき
  • 画面状態の同期漏れを防ぎたいとき
BuilderIO/agent-native7,5272026年10月26日 更新

adhoc-analysis

無料概要あり

複数のデータソースを照合して、傾向や指標、根拠をまとめた分析結果を作成します。必要に応じて、再実行できるダッシュボードとして保存できます。

  • 商談と通話記録を照合したいとき
  • 顧客の利用状況を横断して調べたいとき
  • 繰り返し更新できる分析を保存したいとき
BuilderIO/agent-native7,5272026年10月26日 更新

admin-surfaces

無料概要あり

Analyticsの管理画面で、機能フラグの状態確認と切り替え、ダッシュボード利用状況の調査、接続先アプリのデータベース管理を行います。

  • アプリごとの機能フラグを確認・変更したいとき
  • ダッシュボードの所有者や利用状況を調べたいとき
  • 接続済みアプリのテーブルを管理したいとき
BuilderIO/agent-native7,5272026年10月26日 更新

BuilderIO のスキルをすべて見る

このスキルの問題を報告する