2c9a3fd05dad

fix: PCH-01-021 gate clipboard alarm clear on the clear-owed flag (CLEAR_HASH_KEY) — stale alarm on a cold worker skips offscreen churn; dead bookkeeping is now load-bearing (v3.8.1)

time
2026-06-10 20:20 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
pch-01 fix integration
parent
3867c5eae0a1
2 files src/background/clipboard.test.ts+25 −0src/background/clipboard.ts+18 −1
src/background/clipboard.test.ts+25 −0
@@ -178,6 +178,8 @@178178     setupClipboardClearAlarm();179179     const clipListener = alarmListeners.at(-1)!;180180 181+    // PCH-01-021: the alarm only clears when a clear is still owed.182+    session["clipboard-clear-hash"] = "owed";181183     clipListener({ name: CLIPBOARD_CLEAR_ALARM });182184     await flushMicrotasks();183185 @@ -206,12 +208,33 @@206208     setupClipboardClearAlarm();207209     const clipListener = alarmListeners.at(-1)!;208210 211+    // PCH-01-021: the alarm only clears when a clear is still owed.212+    session["clipboard-clear-hash"] = "owed";209213     clipListener({ name: CLIPBOARD_CLEAR_ALARM });210214     await flushMicrotasks();211215 212216     expect(clipboardWriteText).toHaveBeenCalledWith("");213217   });214218 219+  // PCH-01-021: a stale alarm with no clear owed must skip the clear and drop220+  // the alarm, avoiding pointless offscreen-document churn on a cold worker.221+  it("PCH-01-021: alarm with no clear-owed flag skips the clear and drops the alarm", async () => {222+    const { setupClipboardClearAlarm, CLIPBOARD_CLEAR_ALARM } = await import(223+      "./clipboard"224+    );225+    setupClipboardClearAlarm();226+    const clipListener = alarmListeners.at(-1)!;227+228+    // Empty session → nothing owed.229+    delete session["clipboard-clear-hash"];230+    alarmsClear.mockClear();231+    clipListener({ name: CLIPBOARD_CLEAR_ALARM });232+    await flushMicrotasks();233+234+    expect(clipboardWriteText).not.toHaveBeenCalled();235+    expect(alarmsClear).toHaveBeenCalledWith(CLIPBOARD_CLEAR_ALARM);236+  });237+215238   it("HV-2: a second copy clears the previous alarm before re-arming", async () => {216239     const { copyToClipboard, CLIPBOARD_CLEAR_ALARM } = await import(217240       "./clipboard"@@ -251,6 +274,8 @@251274     const clipListener = alarmListeners.at(-1)!;252275     alarmsCreate.mockClear();253276 277+    // PCH-01-021: a clear is owed (hash present), so the alarm proceeds to clear.278+    session["clipboard-clear-hash"] = "owed";254279     clipListener({ name: CLIPBOARD_CLEAR_ALARM });255280     await flushMicrotasks();256281 

view file

src/background/clipboard.ts+18 −1
@@ -179,6 +179,23 @@179179 export function setupClipboardClearAlarm(): void {180180   chrome.alarms.onAlarm.addListener((alarm) => {181181     if (alarm.name !== CLIPBOARD_CLEAR_ALARM) return;182-    void clearClipboardNow();182+    // PCH-01-021: only clear if a clear is still owed. CLEAR_HASH_KEY is set on183+    // copy and removed once a clear succeeds; its absence means the fast-path184+    // setTimeout (or a prior alarm) already cleared, so a cold worker woken by a185+    // stale alarm can skip the offscreen-document churn.186+    void (async () => {187+      try {188+        const stored = await chrome.storage.session.get(CLEAR_HASH_KEY);189+        if (stored?.[CLEAR_HASH_KEY] === undefined) {190+          // Nothing owed — drop the stale alarm and return.191+          await chrome.alarms.clear(CLIPBOARD_CLEAR_ALARM).catch(() => false);192+          return;193+        }194+      } catch {195+        // session.get failed — fall through and clear anyway. Erring toward196+        // clearing keeps the secret-must-not-persist invariant.197+      }198+      await clearClipboardNow();199+    })();183200   });184201 }

view file