background-state について
background-stateは、Even Hubプラグインに状態の永続化を追加するエージェントスキルです。バックグラウンド移行後も保持する可変状態を特定し、SDKのエクスポート処理と復元処理を挿入します。
主な機能
- 01保持対象の状態の特定
- 02適切なスナップショットキーの選定
- 03SDKのエクスポート・復元処理の追加
- 04シリアライズ可能性と復元時の代替値の確認
活用シーン
- 01バックグラウンド移行後のカウンター維持
- 02選択項目や動作モードの保持
- 03バッファーや履歴の復元
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ even-realities/ everything-evenhub --skill background-stateインストール後、Claude Code で/background-stateを入力して読み込まれたか確認します。
詳細
バックグラウンド状態
コードを分析し、@evenrealities/even_hub_sdk の setBackgroundState と onBackgroundRestore の呼び出しを挿入して、Even Hub プラグインにバックグラウンド状態の永続化を追加します。
背景
Even Hub ホストアプリは、スマートフォンがバックグラウンドに移行するときに Headless WebView 移行方式を使用します。
inactive— ホストがwindow.__getStateSnapshot()を使って現在の JS 状態をスナップショットとして保存しますpaused— ホストが新しいHeadlessInAppWebViewを作成し、同じプラグイン URL を読み込み、保存した状態を再現するためにwindow.__restoreState(snapshot)を呼び出します- バックグラウンド — ヘッドレス WebView が不可視の状態で動作し、引き続きメガネにフレームを送信します
resumed— フォアグラウンドの WebView が起動する前にスナップショットを注入し、その後ヘッドレス WebView が破棄されます
プラグインで状態エクスポーターを登録していない場合、__getStateSnapshot() は '{}' を返し、ヘッドレス WebView は初期状態から起動します。そのため、スマートフォンがバックグラウンドから戻るたびに、プラグインが初期状態にリセットされます。
使用する場面
次のようなプラグインにこのスキルを適用します。
- 時間とともに進むカウンター、スコア、タイマー、連番を表示する
- 選択中の項目、現在のページ、アクティブなモードを追跡する
- リセットされるべきではないデータバッファーや履歴を保持する
- プラグインの実行中に変化し、フォアグラウンド復帰時に一貫している必要がある変数がある
API リファレンス
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
// Register state exporter — called by host before going to background
setBackgroundState('myKey', () => ({ ...myState }))
// Register state restorer — called by host after headless WebView loads
onBackgroundRestore('myKey', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
ルール:
keyは両方の呼び出しで同じ文字列にする必要があります- エクスポーターは プレーンな JSON シリアライズ可能オブジェクトを返す必要があります(クラスインスタンス、関数、Map、Set は使用できません)
- リストアラーが受け取るのは
Record<string, unknown>です。使用する前に必ずキャストしてください - 両方の呼び出しは モジュール初期化時(トップレベル、または起動関数内で
bridge.onEvenHubEventより前)に実行する必要があります - 初期スナップショットが常に有効になるように、状態変数を変更するコードより前に
setBackgroundStateを呼び出してください
ワークフロー
ステップ 1 — ソースを読む
$ARGUMENTS がファイルを指している場合は、そのファイルを直接読みます。ディレクトリを指している場合は、*.ts と *.tsx ファイルを検索し、関連性の高いもの(通常は main.ts、index.ts、または waitForEvenAppBridge を含むファイル)を読みます。
ステップ 2 — スナップショット対象の状態を特定する
次のような変数を探します。
| パターン | 例 | スナップショット対象? |
|---|---|---|
| プリミティブなカウンターまたはタイマー | let counter = 0 / frameIndex++ | ✅ はい |
| 選択中のインデックスまたはアクティブなモード | let selectedIdx = 0 / let mode: 'A'|'B' = 'A' | ✅ はい |
| データバッファーまたは履歴配列 | const history: string[] = [] | ✅ はい |
| 起動時に設定される構成 | let speed = 1 の後に speed = userChoice | ✅ はい |
| 派生値/計算値 | const label = items[selectedIdx].name | ❌ いいえ — リストア後に再導出します |
| WebView のライフサイクルフラグ | let isReady = false | ❌ いいえ — 再初期化されます |
| ブリッジ/コントローラーの参照 | let bridge: EvenAppBridge | ❌ いいえ — シリアライズできません |
| 一時的な UI フラグ | let isAnimating = false | ❌ いいえ — 一時的な状態です |
重要な判断基準:バックグラウンドへの移行時に変数が失われると、バックグラウンドに移行しなかった場合とプラグインの動作が異なるなら、その変数はスナップショット対象です。
ステップ 3 — スナップショットキーを選ぶ
論理的な状態グループごとにキーを 1 つ使います。変数ごとに 1 つのキーを使う(細かすぎる)のも、すべてを 1 つのキーにまとめる(大まかすぎる)のも避けてください。通常、1 つのプラグインで必要なキーは 1~2 個です。
例:
- フレームカウンターには
'counter' - メインの変更可能な状態オブジェクトには
'appState' - 現在選択中の項目のインデックスと関連データには
'selection'
ステップ 4 — コードを生成して挿入する
適切な挿入位置、つまり状態変数の宣言 後、bridge.onEvenHubEvent(...) の 前 を見つけて、次を挿入します。
// Background state persistence — survives host background/foreground migration
setBackgroundState('myKey', () => ({ /* snapshot of mutable state */ }))
onBackgroundRestore('myKey', (saved) => {
const s = saved as { /* type annotation */ }
// restore each field with nullish fallback
})
まだ存在しない場合は、次の import も追加します。
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
ステップ 5 — 正しく実装できているか確認する
挿入後、次の点を確認してください。
- インポート行があり、インポートが重複していない
- どちらの呼び出しも同じキー文字列を使用している
- エクスポーターがスナップショットのコピー(ライブ参照ではなく
{ ...state })を展開している - リストア処理でライブ変数が再代入されている(
savedを読み取るだけではない) - エクスポート対象のオブジェクトにクラスインスタンス、Dates、Maps、Sets が含まれていない
- リストア処理のすべてのフィールドで、現在の値を使うフォールバックとして
??が使われている
完全な例
変更前(バックグラウンドへの遷移ですべての状態が失われます):
import { waitForEvenAppBridge } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
変更後(バックグラウンドでも状態が維持されます):
import { waitForEvenAppBridge, setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
// Background state persistence — survives host background/foreground migration
setBackgroundState('appState', () => ({ counter, lastEvent }))
onBackgroundRestore('appState', (saved) => {
const s = saved as { counter: number; lastEvent: string }
counter = s.counter ?? counter
lastEvent = s.lastEvent ?? lastEvent
})
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
よくある間違い
スナップショットではなく参照を取得する
// ❌ Wrong — exporter returns the live object reference; value changes by the time host reads it
setBackgroundState('state', () => myState)
// ✅ Correct — spread creates a snapshot copy at the moment of export
setBackgroundState('state', () => ({ ...myState }))
リストア処理で読み取るだけで、再代入しない
// ❌ Wrong — local const, live variable unchanged
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
console.log(s.counter) // myState.counter still 0
})
// ✅ Correct — reassign the live variable
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
スナップショットにシリアル化できない値を含める
// ❌ Wrong — Map and Date do not survive JSON round-trip
setBackgroundState('state', () => ({
items: new Map([['a', 1]]),
timestamp: new Date(),
}))
// ✅ Correct — convert to plain types
setBackgroundState('state', () => ({
items: Object.fromEntries(itemsMap),
timestamp: Date.now(),
}))
リストア処理で nullish ガードが抜けている
// ❌ Wrong — crashes or resets to 0 if field missing in snapshot
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter
})
// ✅ Correct — keep current value as fallback when field absent
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter ?? counter
})
条件文またはイベントハンドラーの内部で登録する
// ❌ Wrong — if this code path isn't hit before background, state is not saved
bridge.onEvenHubEvent(event => {
if (event.sysEvent?.eventType === 4) {
setBackgroundState('state', () => ({ counter })) // too late
}
})
// ✅ Correct — always register at module init time
setBackgroundState('state', () => ({ counter }))
bridge.onEvenHubEvent(event => { /* ... */ })
タスク
$ARGUMENTS のプラグインコードを分析してください。上記の表に基づいて、スナップショット対象となるすべての状態変数を特定してください。その後、適切な場所に setBackgroundState と onBackgroundRestore を挿入してください。次の内容を要約して報告してください。
- 特定した状態変数と、スナップショット対象となる理由
- 対象外としたものと、その理由
- 選択したキーと、挿入したコードの正確な内容