The test races stale and fresh worker calls to verify that invalidation
prevents stale results from populating the cache. On slow CI, the
fresh worker mock could be reached before the stale deferred was
resolved, causing the version guard to mismatch.
Flush microtasks after starting freshPromise so it advances past
internal awaits and reaches the worker mock before we resolve the
stale deferred.
P1: Poller no longer overwrites nextCursor/hasMore — those belong
to the "Load older" flow. Both poller and loadOlder now dedup
messages by messageId or timestamp+from fingerprint.
P1: Cursor is now compound (timestamp|messageId) with stable
tie-breaking sort. Messages sharing the same timestamp at page
boundaries are no longer lost.
P2: getMessagesPage now applies the same enrichment as getTeamData:
leadSessionId propagation and slash-command-result annotation.
P3: Added 3 tests for getMessagesPage covering pagination, cursor
stability with same-timestamp messages, and slash command annotation.
Two different interval sets with the same length would produce
the same cache key, returning stale results. Serialize each
interval's startedAt~completedAt into the key.
capTimerMap was cancelling oldest pending throttle/debounce timers
when the Map exceeded a size limit. Since the schedulers don't
replay dropped keys, this silently lost refresh callbacks — leaving
session/team state stale after high-volume file change events.
These Maps are self-cleaning (entries delete themselves when their
callback fires) and hold ~100 bytes per entry. Even 200 entries
is negligible memory. Removing the cap fixes the data freshness
issue without any memory concern.
Returning messages: [] broke the slash command annotation test and
any code relying on getTeamData.messages (notifications, dedup).
Keep a small batch (50 newest) in getTeamData for compatibility.
Full message history is available via getMessagesPage() API.
New cursor-based IPC endpoint for the messages timeline panel:
- team:getMessagesPage(teamName, { beforeTimestamp?, limit? })
returns { messages, nextCursor, hasMore }
- Cursor is timestamp-based — stable under live message insertion
- Default page size 50, max 200
getTeamData no longer includes messages in its response, eliminating
the ~1MB messages payload from every team refresh. Messages are now
fetched independently by MessagesPanel.
MessagesPanel changes:
- Fetches initial page on mount via getMessagesPage API
- Auto-refreshes newest page every 5s when team is alive
- "Load older messages" button for pagination
- Falls back to prop messages if API fails (graceful degradation)
The TeamDataWorkerClient logged console.warn when the worker file
was not found, which is expected during tests (no build output).
The test setup treats unexpected warnings as failures.
Downgrade to logger.debug for the "not found" message and remove
the eager warning from resolveWorkerPath().
- Move endRefreshing() outside the !cancelled guard in finally block
so it always runs even when the effect is torn down mid-refresh.
- Only call load() when isTabActive or on first load — prevents
unnecessary fetches when a hidden tab's effect re-runs.
Critical:
- findLogsForTask: call worker directly instead of wrapping in
wrapTeamHandler, so failures propagate to catch and trigger the
main-thread fallback correctly.
- TeamDataWorkerClient: scope error/exit handlers to the specific
worker instance to prevent a stale worker's exit event from
rejecting pending requests on a replacement worker (race condition).
Major:
- TeamDataWorkerClient: validate teamName and taskId before forwarding
to worker thread (input sanitization).
- team-data-worker: include status, since, and intervals length in
cache key to prevent stale results after filter changes.
- team-data-worker: move logsInFlight.delete() into .finally() so
rejected lookups don't poison the in-flight map permanently.
- MemberLogsTab: reset refreshCountRef and refreshing state in effect
cleanup to prevent the refresh indicator from latching on permanently
when the effect tears down mid-refresh (e.g. on tab switch).
The user inbox (user.json) contains real teammate-to-user messages
generated by Claude Code CLI. Filtering it as a system inbox was
incorrect — it broke message aggregation for user-directed messages.
Only the broadcast inbox (*.json) needs to be excluded since '*'
is not a valid member name and causes a phantom member in the UI.
The broadcast inbox file (inboxes/*.json) was being parsed by
listInboxNames() as a member named "*", which appeared in the UI
as a phantom team member. Since "*" fails the MEMBER_NAME_PATTERN
validation, it could not be removed through the UI.
Filter system inbox names (*, user) from listInboxNames() so they
are not treated as real team members.
Main process — worker thread for team data:
- New team-data-worker thread handles getTeamData and findLogsForTask,
isolating heavy file I/O (scanning 300+ subagent JSONL files) from
Electron's main event loop. getTeamData dropped from ~2000ms on the
main thread to ~110ms via the worker.
- Worker-side dedup and 10s result cache for findLogsForTask prevents
redundant scans when the same task is queried multiple times.
- Discovery cache TTL raised from 5s to 30s — avoids re-scanning the
entire project directory on every call.
- Message cap at 200 in TeamDataService to keep IPC payloads under 1MB
(was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization).
- IPC handlers fall back to main-thread execution if the worker is
unavailable (graceful degradation).
Renderer — useShallow and memoization (55 files):
- Added useShallow to store selectors across 55 renderer files. Batched
individual useStore() calls (e.g. 17 calls in ExtensionStoreView,
10 in ConnectionSection) into single useShallow selectors, cutting
unnecessary re-render checks on every store update.
- MemberLogsTab: three 5-second polling intervals now pause when the
parent tab is hidden (display:none). Previously 5 hidden tabs × 3
intervals = 15 polling timers firing continuously.
- KanbanColumn wrapped in React.memo to skip re-renders when props
haven't changed.
- MemberList: memoized activeMembers/removedMembers/colorMap; replaced
O(n×m) per-member task scan with a pre-computed reviewer map.
- Bounded timer Maps in store initialization to prevent unbounded growth
of debounce/throttle tracking maps during long sessions.