Teams Web read strategies

Read-only harvest paths for forge-lcdl Teams tasks. All paths use an attached Playwright session (prefer Edge CDP with a signed-in M365 profile).

Updated

Consumer orchestration (checkpoints, unified_message, sync UI) lives in the host app — see Messaging consumer boundary.

Strategy selection

Goal Governed task Notes
Gate before sync teams_probe_health Surface readiness
Cross-chat date window teams_search_messages_chunk Global search: *, Messages scope, date from/to, Next until disabled, keyboard navigation to open highlighted row
Chat inventory teams_list_chats Sidebar list; may return 0 when rail not harvestable
Channel inventory teams_list_channels Team channels
Bulk chat messages (chunked) teams_list_chat_messages_chunk chunk_size + checkpoint / next_cursor; scroll direction up for older history
Channel post batches teams_list_channel_posts_chunk Team/channel threads
Thread replies teams_read_thread_replies Under a post URL

Date-window / multi-chat (production primary)

When the consumer sets a harvest window (e.g. past 7 days) or teams_list_chats is empty:

  1. teams_probe_health with session_ref and Teams v2 URL.
  2. teams_search_messages_chunk — open global search (AUTOSUGGEST_INPUT or Ctrl+Alt+E), query *, select Messages, apply date_from / date_to, paginate with Next until disabled, activate each result (keyboard or click) to open the highlighted message, harvest row fields. Prefer composing from public atoms in connectors/teams_search_atoms.py (scope verify + per-page guard). Optional discover-only probe: teams_probe_day_pagination (cockpit: scripts/run-teams-search-pagination-harness.py or G0 scripts/run-teams-search-atoms-smoke.py).
  3. Map items[] → consumer model; dedupe on Exchange item_id / message_identity_key.
  4. Fall back to sidebar chat loop (below) for rows search missed.
  1. teams_probe_health with session_ref and channel entry_url (Teams v2).
  2. Optional: teams_list_chats — iterate up to max_chats.
  3. teams_list_chat_messages_chunk with chunk_size 40–100; scroll_direction=up for date windows; pass next_cursor as checkpoint until chunk.has_more is false.
  4. Map each items[] row → consumer message model; dedupe on stable id (data-mid / item_id when present).
  5. Optional: teams_read_thread_replies when list rows lack full body text.

Not a substitute for global search: per-chat Find in chat (Ctrl+F) only searches the open conversation.

Keyboard navigation (production default: COCKPIT_TEAMS_NAV_MODE=keyboard)

Accessible keyboard paths — no mouse clicks or programmatic scroll unless keyboard step fails (click mode is legacy/debug only).

Step Keyboard Fallback (COCKPIT_TEAMS_NAV_MODE=click)
Open search Ctrl+Alt+E → focus AUTOSUGGEST_INPUT Toggle search button
Messages tab search_focus_messages_tabEnter search_click_messages_tab
Query sync Focus input → Ctrl+A*Enter Same with click-to-focus
Date chip Focus [data-tid="search-date-filter"]Enter/Space search_prepare_date_panel click
From / To Keyboard type Jun D, YYYYEnterTab (no calendar clicks) Calendar pick + input fallback
Apply search_focus_date_applyEnter search_click_date_apply
SERP load Focus results → PageDown / End (search_keyboard_scroll_step) search_scroll_step (scrollTop)
SERP row Focus role="row" + Enter search_click_card
Next page Focus Next + Enter search_click_next

LCDL module: connectors/teams_search_keyboard.py. Chunk stats include nav_method in chunk_meta warnings.

Production default: Cockpit ladder/EOD scripts export COCKPIT_TEAMS_NAV_MODE=keyboard. Before re-running EOD/ladder, stop in-flight harvest: ./scripts/stop-teams-search-ingest.sh (Cockpit). Consumer applies serp_day_scoped ingest trust when the date chip matches the scan day — see Cockpit teams-search-day-ingest.md.

Search ingest atoms (teams_search_atoms.py)

Test-first public API — compose EOD/remediation without importing private teams_search_chunk helpers:

Atom Function
Surface teams_ensure_search_surface
Open teams_open_messages_search
Scope teams_apply_single_day_scope
Verify teams_verify_single_day_scopeScopeVerdict (date chip canonical; query-day scope is drift)
Guard teams_guard_single_day_scope (re-apply + FORGE_LCDL_TEAMS_MAX_CARDS_PER_DAY)
Prepare teams_prepare_serp_surface
Harvest teams_harvest_serp_cards
Paginate teams_advance_serp_next_page
Detail teams_harvest_card_detail
Restore teams_restore_serp_after_detail
Probe teams_probe_day_pagination

CI gate: pytest tests/playwright/test_teams_search_atoms_*.py before live EOD (run-teams-search-last-week-eod.sh).

Read-only policy

Same session contract as Outlook: navigate, evaluate_readonly, scroll_readonly, limited click_readonly for list focus only. No send, react, call, or meeting joins. See READONLY_POLICY.md.

CDP setup

Same as Outlook: consumer starts Edge with remote debugging, signs in to https://teams.microsoft.com/v2/, registers the page, then calls LCDL tasks. Cockpit documents this in its messaging boundary handbook.

Tab visibility: Teams virtualized chat lists behave like OWA — background tabs may not render rows. LCDL enables CDP focus emulation on attach when bring-to-front is off (FORGE_LCDL_CDP_BRING_TO_FRONT=0, COCKPIT_EDGE_FOCUS_EDGE=0). Do not rely on page.bring_to_front() for production ingest. See EDGE-CDP-ATTACH.md.

Timing

Task Typical live
teams_probe_health 5–30 s (navigation + settle)
teams_list_chat_messages_chunk 30 s – several min per chunk (virtualized list)

Consumers should emit heartbeat progress during long run_task calls so operators do not assume a stuck browser step.

Example consumer bridge (reference only)

forge-cockpit-web implements cockpit_server.lcdl_teams_bridge (harvest_chat_chunk_via_lcdl, run_task). LCDL does not import that module.