promptward

Plan 011: Take debug logging off the send critical path and serialize log appends

Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the “STOP conditions” section occurs, stop and report — do not improvise. When done, update the status row for this plan in plans/README.md — unless a reviewer dispatched you and told you they maintain the index.

Drift check (run first): git diff --stat a6293e1..HEAD -- src/content.ts src/background.ts tests/ Plans 001–004 are expected to have modified src/content.ts and its tests. The specific excerpts to re-verify are the await logDebug( call sites in the protect flow and appendDebugEvent in src/background.ts.

Status

Why this matters

Every send is delayed by diagnostics. The protect flow in src/content.ts awaits logDebug five-plus times before and around the model call; each call is a chrome.runtime.sendMessage round trip to the background worker, which then does a storage.session read + write per event (appendDebugEvent). That is serialized latency added to every single send — for bookkeeping the user never sees. Separately, appendDebugEvent is a read-modify-write with no serialization: concurrent events (two tabs, or the burst a single send produces across content/background/offscreen/worker contexts) interleave reads and overwrite each other, losing events — the exact tool you need when debugging a race is itself racy.

Fix both: fire-and-forget logging in the content flow (ordering preserved via the existing ts timestamp), and a promise-chain queue in the background so appends serialize.

Current state

Commands you will need

Purpose Command Expected on success
Typecheck npx tsc -p tsconfig.json --noEmit exit 0
Tests npm test all pass

Scope

In scope:

Out of scope:

Git workflow

Steps

Step 1: Fire-and-forget in the content flow

In src/content.ts, inside protectAndMaybeSubmit (and handleProtectResponse if plan 003 extracted it), change await logDebug({...}) to void logDebug({...}) at every stage EXCEPT leave untouched any call whose result the code depends on (none do — verify by reading each site). Keep the await-free style consistent with the existing void logDebug(...) calls in the event handlers (onClickCapture etc.).

Two sites need care:

Do NOT change the logDebug implementation itself.

Verify: npx tsc -p tsconfig.json --noEmit → exit 0 (the noUnusedLocals strict config will surface any summary variable you orphaned). Verify: npx vitest run tests/content-flow.test.ts → all pass. If a test asserted on stub.sentMessages counts that included debug messages synchronously, adapt it with vi.waitFor.

Step 2: Serialize appends in the background

In src/background.ts, wrap appendDebugEvent in a module-level chain:

let appendQueue: Promise<void> = Promise.resolve();

function appendDebugEvent(event: DebugEvent): Promise<void> {
  const task = appendQueue.then(async () => {
    const events = await loadDebugLogs();
    const next = [...events, event].slice(-DEBUG_LOG_LIMIT);
    await chrome.storage.session.set({ [DEBUG_LOGS_KEY]: next });
    console.debug("[PromptWard]", event);
  });
  appendQueue = task.catch(() => undefined); // one failure must not wedge the queue
  return task;
}

Keep the existing await appendDebugEvent(...) call sites in background.ts as-is: within one message they’re sequential anyway, and the queue now makes cross-message interleaving safe. (The latency these awaits add is one storage round trip each in the service worker, not a content-page send stall — reducing them further is not worth the reordering risk.)

Verify: npx tsc -p tsconfig.json --noEmit → exit 0.

Step 3: Full suite

Verify: npm test → exit 0.

Test plan

Regression cover is tests/content-flow.test.ts (flow behavior unchanged) plus tests/background-offscreen.test.ts if plan 005 landed. Optional: if that background harness exists, add one test — dispatch two PW_DEBUG_LOG messages whose stubbed storage.session.get resolves on a manual trigger, release both, assert the final set call contains BOTH events (the queue prevents the lost-update). Skip if the harness doesn’t exist; do not build one for this.

Done criteria

STOP conditions

Maintenance notes