feat: enrich chat search results with full session context#3959
feat: enrich chat search results with full session context#3959
Conversation
Add load_session_content command to fs-sync plugin to read session data (meta, memo, transcript, notes) from disk. Hydrate search results with resolved participants, speaker-labeled transcripts, and rendered template output so the chat model receives rich context for referenced sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
✅ Deploy Preview for hyprnote-storybook canceled.
|
✅ Deploy Preview for hyprnote canceled.
|
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
| const isHumanAssignment = | ||
| !!store && | ||
| typeof speakerId === "string" && | ||
| Boolean(store.getRow("humans", speakerId)); |
There was a problem hiding this comment.
🔴 Boolean(store.getRow(...)) always returns true for non-existent rows
In session-context-hydrator.ts:100-103, the check Boolean(store.getRow("humans", speakerId)) is used to determine whether a speakerId corresponds to a real human in the store. However, TinyBase's getRow returns an empty object {} when the row doesn't exist, and Boolean({}) is always true in JavaScript. This means isHumanAssignment will be true for every speaker hint where store exists and speakerId is a string, even when the speakerId doesn't correspond to an actual human.
Root Cause and Impact
The code at line 103:
Boolean(store.getRow("humans", speakerId))always evaluates to true because {} is truthy. This causes every speaker hint to be classified as user_speaker_assignment instead of provider_speaker_index, producing incorrect speaker labels in hydrated transcripts.
Notably, the correct pattern is already used at session-context-hydrator.ts:172-173 for participant resolution:
const row = store?.getRow("humans", participant.humanId);
if (!row || typeof row.name !== "string" || !row.name) {
return null;
}Here, the code correctly handles the empty-object case by checking for row.name. The speaker hint check should use a similar approach.
Impact: All transcript speaker labels will be resolved through the human-assignment path (looking up a human name), which will fail for provider-assigned speaker indices. This leads to wrong or missing speaker names in the rendered transcript context sent to the chat model.
| const isHumanAssignment = | |
| !!store && | |
| typeof speakerId === "string" && | |
| Boolean(store.getRow("humans", speakerId)); | |
| const isHumanAssignment = | |
| !!store && | |
| typeof speakerId === "string" && | |
| Object.keys(store.getRow("humans", speakerId)).length > 0; | |
Was this helpful? React with 👍 or 👎 to provide feedback.
| let frontmatter_session_id = frontmatter | ||
| .get("session_id") | ||
| .and_then(|v| v.as_str()) | ||
| .unwrap_or("") | ||
| .to_string(); | ||
|
|
||
| if frontmatter_session_id != session_id { | ||
| continue; | ||
| } | ||
|
|
||
| if name == SESSION_MEMO_FILE { | ||
| content.raw_memo_tiptap_json = Some(tiptap_json); | ||
| continue; |
There was a problem hiding this comment.
🚩 Session memo file matching uses session_id frontmatter check that may exclude valid memos
In session_content.rs:80-82, markdown files are skipped if frontmatter_session_id != session_id. The session_id is read from frontmatter at line 74-78 with a fallback to empty string. If a memo file's frontmatter lacks a session_id key entirely, frontmatter_session_id becomes "" and won't match the actual session ID, causing the memo to be silently skipped. However, this is handled for _memo.md specifically — the name == SESSION_MEMO_FILE check at line 84 comes after the session_id check at line 80, so a memo without a session_id in its frontmatter would indeed be dropped. If real _memo.md files don't always include session_id in frontmatter, this could cause data loss in the hydrated context.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
load_session_contentcommand to thefs-syncplugin to read session meta, memo, transcript, and notes from disksession-context-hydratorthat transforms raw fs data into richSessionContextwith resolved participants and speaker-labeled transcript segmentsToolSearchSessionstemplate and reusablesession_context_detailsmacro so search results are rendered with full context for the chat modelChatSystemto removerelatedSessionsfield; simplifyprompt-contextlogicSupportSystem→SupportContextin template bindingsUpdates since last revision
session-context-hydrator.ts: added optional chaining (?.map()) onpayload.meta?.participantsto prevent crash whenmetais null (from Graphite review)test_chat_system_with_contextto match actual template output (extra blank line fromsession_context_detailsmacro)Review & Testing Checklist for Human
relatedSessionsonChatSystem— this field was removed entirely. Search codebase for any remaining usage.SupportSystem→SupportContextrename is complete — check thatsupport-block.tsand all template bindings use the new key (supportContextnotsupportSystem)session-context-hydrator.tstranscript building logic — the speaker hint resolution and word-index mapping is complex and not covered by automated tests. Manually verify with a real session that has speaker hints.load_session_contentwith sessions that have missing files (e.g. no_meta.json, notranscript.json) — verify graceful fallback tonullfieldsNotes
cigate job (exit 1) was failing because thedesktop_cisnapshot test failed — now fixed.parseSessionContextfunction incontext-item.tsdoes runtime parsing ofunknownvalues from tool output. If the tool output shape changes, this will silently return nulls rather than error.Made with Cursor
Link to Devin run: https://app.devin.ai/sessions/ceaec159fd40472aa2dd1d774cd0594c
Requested by: @yujonglee