Skip to content

feat(people): on-demand people data layer with recency + semantic search - #351

Merged
cabcookie merged 2 commits into
mainfrom
feat/people-data-layer
Sep 23, 2026
Merged

cabcookie merged 2 commits into
mainfrom
feat/people-data-layer

Conversation

@cabcookie

Copy link
Copy Markdown
Owner

Builds on #350 (base branch: feat/sonic-meeting-stage2).

Problem

The app loaded ALL people into memory (Person.list, limit 4000) and every
consumer did people.find(id). Anyone not in that set rendered no name — e.g. a
meeting's participants ("Matt", "David") showed blank. It also doesn't scale.

Solution — incremental, cache-and-keep store + semantic search

Backend

  • Person.lastSeen (datetime) + sparse GSI listPeopleByLastSeen (owner PK,
    lastSeen SK). Initial in-memory set = the 50 most recently seen people.
  • touch-person-last-seen Lambda on the MeetingParticipant / NoteBlockPerson
    DynamoDB streams (INSERT): forward-only, owner-preserving UpdateItem stamps
    Person.lastSeen. No debounce. CDK-wired.
  • searchPeople query: semantic vector search for typed lookups, reusing the
    existing person-vector-search Lambda (owner enforced server-side).

Store (api/usePeople.ts)

  • Cache-and-keep map wrapped by SWR; loads recent-50 initially, never evicts.
  • getPersonById / getPeopleByIds fetch missing people on demand by id and merge
    them in — a referenced person is never permanently invisible.
  • searchPeopleRemote / queryPerson use the vector search; resolvePeopleByIds for
    SWR fetchers.

Consumers migrated off people.find()

people-selector + Cmd-K palette (debounced semantic search; ComboBox gains an
optional onSearch that disables client-side filtering), meeting participants,
PeopleList, activity-meeting-list, payer person/header, resolveParticipant,
useInvolvedPeople, and all mention editors.

Dev scripts

  • copy-recent-meetings-to-sandbox.js — copy last N days of PROD meetings + all
    relations (incl. NoteBlockPerson) into the sandbox.
  • backfill-person-last-seen.js — seed Person.lastSeen from existing relations.

Deploy note

The lastSeen GSI replaces the Person table on deploy (Amplify recreates the
table on a secondary-index change). Sandbox was redeployed, last-28-days PROD
meetings copied in, and lastSeen backfilled.

Testing

  • tsc --noEmit (root) — green
  • tsc --noEmit -p amplify/tsconfig.json — green
  • npm run lint — 0 errors (2 pre-existing TanStack Table warnings)
  • Sandbox deployed; recent meetings + participants render names; typed people
    search returns semantic matches.

Replaces "load all people into memory" (Person.list limit 4000) — which made
a referenced person invisible if it wasn't in the loaded set (e.g. a meeting's
participants showing no names) — with an incremental, cache-and-keep store.

Data layer:
- Person.lastSeen (datetime) + sparse GSI listPeopleByLastSeen (owner PK,
  lastSeen SK). Initial set is now the 50 most recently seen people.
- touch-person-last-seen Lambda: off the MeetingParticipant / NoteBlockPerson
  DynamoDB streams (INSERT), stamps Person.lastSeen via a forward-only,
  owner-preserving UpdateItem. No debounce. CDK-wired in custom/backend.
- searchPeople query: semantic (vector) search for typed lookups, sharing the
  existing person-vector-search Lambda (owner enforced server-side).

Store (api/usePeople.ts):
- Module-level cache-and-keep map wrapped by SWR; loads recent-50 initially.
- getPersonById / getPeopleByIds fetch missing people on demand by id and
  merge them in (a referenced person is never permanently invisible).
- searchPeopleRemote + queryPerson use the vector search and merge matches.
- resolvePeopleByIds for non-render callers (SWR fetchers).

Consumers migrated off people.find():
- people-selector + Cmd-K palette: debounced semantic search (ComboBox gains
  an optional onSearch that disables client-side filtering).
- meeting participants, PeopleList, activity-meeting-list, payer person/header,
  resolveParticipant, useInvolvedPeople, mention editors.

Dev scripts:
- copy-recent-meetings-to-sandbox.js: copy last N days of PROD meetings (+ all
  relations incl. NoteBlockPerson) into the sandbox.
- backfill-person-last-seen.js: seed Person.lastSeen from existing relations.

Note: the lastSeen GSI replaces the Person table on deploy (Amplify recreates
the table on a secondary-index change).
@cabcookie
cabcookie changed the base branch from feat/sonic-meeting-stage2 to main September 23, 2026 07:28
@cabcookie
cabcookie merged commit 47e0885 into main Sep 23, 2026
1 check passed
@cabcookie
cabcookie deleted the feat/people-data-layer branch September 23, 2026 07:37
cabcookie added a commit that referenced this pull request Sep 25, 2026
…etch) (#355)

Applies the people data-layer principles (PR #351) to projects and fixes
the prod list silently showing < 10 projects.

Root cause: client.models.Projects.list() returned only the first DynamoDB
page (~1 MB) and never followed nextToken; once PR #348 added the heavy
projectSummary markdown to the list selectionSet, items grew so large that a
page held only a handful of the (1146) projects.

Changes:
- Split selectionSet into leanSelectionSet (list/active set, drops the
  projectSummary full text, keeps the tiny projectSummaryUpdatedAt) and
  fullSelectionSet (on-demand by-id: detail page, Sonic context). The
  summaryEmbedding vector was never client-selected (native DDB write).
- fetchProjects now paginates via nextToken (limit 1000) so the active set is
  complete again, with much smaller pages.
- Module-level project cache (cache-and-keep): loadProjectsByIds /
  resolveProjectsByIds / searchProjectsRemote / ensureProjectSummaries, with a
  cache-version SWR key to re-render consumers. getProjectById merges the full
  summary onto the fresh active object; getProjectNamesByIds loads missing ids
  on demand so a referenced done/archived project is never permanently
  invisible.
- searchProjects AI query (reuses the existing project-vector-search Lambda /
  ProjectVoiceMatch) for typed semantic lookups.
- useMeetingSonicData: pre-warm full summaries (ensureOpenProjectSummaries) at
  recording start; getOpenProjects reads them from the cache.
- project-details: ensureProjectSummaries on mount to render the summary.

Verified: tsc --noEmit clean, eslint clean, next build succeeds.

Co-authored-by: Carsten Koch <carstenbkoch@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant