tracegist-mcp-bridge 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -866,6 +866,14 @@ const pendingQuestions = [];
866
866
  /** @type {Map<string, object>} */
867
867
  const questionResponses = new Map();
868
868
 
869
+ /**
870
+ * Persistent log of all agent-tester interactions during the live session.
871
+ * Survives until the next session-start. Used to build the complete interaction
872
+ * history for the final artifact.
873
+ * @type {Array<{type: string, questionId: string, question?: string, timestamp: number, transcription?: string, screenshot?: boolean, highlightCount?: number, dismissed?: boolean}>}
874
+ */
875
+ const liveInteractions = [];
876
+
869
877
  /** @type {Array<{resolve: Function, reject: Function, timeout: ReturnType<typeof setTimeout>}>} */
870
878
  const screenshotWaiters = [];
871
879
 
@@ -949,9 +957,12 @@ async function transcribeLiveVoice(voiceBlobDataUrl) {
949
957
  return { error: "No transcription available (set OPENROUTER_API_KEY or install Whisper)" };
950
958
  }
951
959
 
952
- /** Push a live event into the ring buffer with auto-incrementing seq. */
960
+ /** Push a live event into the ring buffer with auto-incrementing seq.
961
+ * Seq is always higher than any existing event (including extension-originated ones). */
953
962
  function pushLiveEvent(eventType, data) {
954
- liveEventSeq++;
963
+ // Ensure our seq is always higher than the max seq in the buffer
964
+ const maxExistingSeq = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1].seq : 0;
965
+ liveEventSeq = Math.max(liveEventSeq, maxExistingSeq) + 1;
955
966
  liveEvents.push({ seq: liveEventSeq, ts: Date.now(), eventType, data });
956
967
  if (liveEvents.length > LIVE_SHADOW_EVENT_BUFFER_SIZE) {
957
968
  const evictCount = Math.floor(LIVE_SHADOW_EVENT_BUFFER_SIZE * 0.25);
@@ -1037,8 +1048,10 @@ function handleExtensionMessage(msg) {
1037
1048
  startedAt: Date.now(),
1038
1049
  };
1039
1050
  liveEvents.length = 0;
1051
+ liveEventSeq = 0;
1040
1052
  pendingQuestions.length = 0;
1041
1053
  questionResponses.clear();
1054
+ liveInteractions.length = 0;
1042
1055
  console.error(`[${BRIDGE_NAME}] Live shadow: session started (${msg.sessionId})`);
1043
1056
  // Notify MCP clients that resources changed
1044
1057
  try {
@@ -1072,6 +1085,62 @@ function handleExtensionMessage(msg) {
1072
1085
  });
1073
1086
  const rIdx = pendingQuestions.findIndex((q) => q.questionId === msg.questionId);
1074
1087
  if (rIdx >= 0) pendingQuestions.splice(rIdx, 1);
1088
+ // Push a live event so polling agents see the response immediately
1089
+ pushLiveEvent("question-response-received", {
1090
+ questionId: msg.questionId,
1091
+ hasVoice: !!msg.voiceBlobDataUrl,
1092
+ hasScreenshot: !!msg.screenshot,
1093
+ highlightCount: msg.highlightCaptures?.length || 0,
1094
+ hint: "Use get_tester_response to retrieve the full response with transcription and images.",
1095
+ });
1096
+ // Eagerly transcribe the voice response and log the interaction immediately
1097
+ // (don't wait for get_tester_response to be called)
1098
+ if (msg.voiceBlobDataUrl) {
1099
+ transcribeLiveVoice(msg.voiceBlobDataUrl)
1100
+ .then((result) => {
1101
+ // Store transcription with the response for later retrieval
1102
+ const stored = questionResponses.get(msg.questionId);
1103
+ if (stored && !stored.dismissed) {
1104
+ stored.transcription = result.transcription || null;
1105
+ }
1106
+ // Log the interaction with transcription
1107
+ liveInteractions.push({
1108
+ type: "tester-response",
1109
+ questionId: msg.questionId,
1110
+ timestamp: msg.timestamp || Date.now(),
1111
+ transcription: result.transcription || null,
1112
+ screenshot: !!msg.screenshot,
1113
+ highlightCount: msg.highlightCaptures?.length || 0,
1114
+ });
1115
+ // Push transcription as a live event so polling agents see it
1116
+ pushLiveEvent("question-response-transcribed", {
1117
+ questionId: msg.questionId,
1118
+ transcription: result.transcription || null,
1119
+ error: result.error || undefined,
1120
+ });
1121
+ })
1122
+ .catch((err) => {
1123
+ console.error(`[${BRIDGE_NAME}] Question response transcription failed:`, err);
1124
+ liveInteractions.push({
1125
+ type: "tester-response",
1126
+ questionId: msg.questionId,
1127
+ timestamp: msg.timestamp || Date.now(),
1128
+ transcription: null,
1129
+ screenshot: !!msg.screenshot,
1130
+ highlightCount: msg.highlightCaptures?.length || 0,
1131
+ });
1132
+ });
1133
+ } else {
1134
+ // No voice — log interaction immediately
1135
+ liveInteractions.push({
1136
+ type: "tester-response",
1137
+ questionId: msg.questionId,
1138
+ timestamp: msg.timestamp || Date.now(),
1139
+ transcription: null,
1140
+ screenshot: !!msg.screenshot,
1141
+ highlightCount: msg.highlightCaptures?.length || 0,
1142
+ });
1143
+ }
1075
1144
  break;
1076
1145
  }
1077
1146
 
@@ -1079,6 +1148,14 @@ function handleExtensionMessage(msg) {
1079
1148
  questionResponses.set(msg.questionId, { dismissed: true });
1080
1149
  const dIdx = pendingQuestions.findIndex((q) => q.questionId === msg.questionId);
1081
1150
  if (dIdx >= 0) pendingQuestions.splice(dIdx, 1);
1151
+ // Log dismissal in interactions
1152
+ liveInteractions.push({
1153
+ type: "question-dismissed",
1154
+ questionId: msg.questionId,
1155
+ timestamp: Date.now(),
1156
+ dismissed: true,
1157
+ });
1158
+ pushLiveEvent("question-dismissed", { questionId: msg.questionId });
1082
1159
  break;
1083
1160
  }
1084
1161
 
@@ -1099,6 +1176,13 @@ function handleExtensionMessage(msg) {
1099
1176
  transcription: result.transcription || null,
1100
1177
  error: result.error || undefined,
1101
1178
  });
1179
+ // Log voice marker in interactions for the final artifact
1180
+ liveInteractions.push({
1181
+ type: "voice-marker",
1182
+ questionId: markerId,
1183
+ timestamp: Date.now(),
1184
+ transcription: result.transcription || null,
1185
+ });
1102
1186
  })
1103
1187
  .catch((err) => {
1104
1188
  console.error(`[${BRIDGE_NAME}] Live voice transcription failed:`, err);
@@ -1151,9 +1235,17 @@ server.resource(
1151
1235
  // Tool: watch_live_session
1152
1236
  server.tool(
1153
1237
  "watch_live_session",
1154
- "Watch a live TraceGist shadowing session. Returns buffered events since the given sequence number. " +
1155
- "Poll every 3-5 seconds to watch events in real-time. " +
1156
- "Returns sessionActive: false when the session ends.",
1238
+ "Watch a live TraceGist shadowing session in real-time. Returns buffered events since the given sequence number.\n\n" +
1239
+ "IMPORTANT POLLING BEHAVIOR:\n" +
1240
+ "- You MUST keep calling this tool every 2-4 seconds in a loop until sessionActive becomes false.\n" +
1241
+ "- Do NOT stop polling just because there are no new events — the tester is still active.\n" +
1242
+ "- When you see 'voice-transcription' events, the tester recorded a voice marker — read the transcription and react to it.\n" +
1243
+ "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer with images.\n" +
1244
+ "- When you see 'question-response-transcribed' events, the tester's voice response has been transcribed — read it for context.\n" +
1245
+ "- Marker events with hasPendingVoice=true will have their voiceTranscription field populated once transcription completes.\n" +
1246
+ "- The tester may set voice markers at any time to communicate with you — keep watching for them.\n" +
1247
+ "- If you need clarification, use ask_tester_question to show a toast notification to the tester.\n" +
1248
+ "- Only stop polling when sessionActive is false (the recording ended).",
1157
1249
  {
1158
1250
  since_seq: z
1159
1251
  .number()
@@ -1172,37 +1264,88 @@ server.tool(
1172
1264
 
1173
1265
  const latestSeq = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1].seq : 0;
1174
1266
 
1267
+ // Enrich marker events with their transcriptions (if available in the buffer)
1268
+ // so agents don't have to correlate separate marker + voice-transcription events
1269
+ const transcriptionsByMarkerId = new Map();
1270
+ for (const e of liveEvents) {
1271
+ if (e.eventType === "voice-transcription" && e.data?.markerId) {
1272
+ transcriptionsByMarkerId.set(e.data.markerId, e.data.transcription);
1273
+ }
1274
+ }
1275
+ const enrichedEvents = events.map((e) => {
1276
+ if (e.eventType === "marker" && e.data?.markerId && transcriptionsByMarkerId.has(e.data.markerId)) {
1277
+ return {
1278
+ ...e,
1279
+ data: {
1280
+ ...e.data,
1281
+ voiceTranscription: transcriptionsByMarkerId.get(e.data.markerId),
1282
+ },
1283
+ };
1284
+ }
1285
+ return e;
1286
+ });
1287
+
1288
+ const responseData = {
1289
+ sessionActive: liveSessionActive,
1290
+ session: liveSessionMeta,
1291
+ events: enrichedEvents,
1292
+ latestSeq,
1293
+ eventCount: enrichedEvents.length,
1294
+ };
1295
+
1296
+ if (liveSessionActive) {
1297
+ responseData.pollingGuidance =
1298
+ "KEEP POLLING every 2-4 seconds. Do NOT stop — the tester is still active. " +
1299
+ "Watch for 'voice-transcription' events (tester voice markers) and 'question-response-received' events. " +
1300
+ "React to voice markers by acknowledging them or asking follow-up questions.";
1301
+ responseData.availableActions = [
1302
+ {
1303
+ tool: "ask_tester_question",
1304
+ description:
1305
+ "Ask the tester a short question (max 200 chars). They respond with voice + highlights.",
1306
+ },
1307
+ {
1308
+ tool: "get_live_screenshot",
1309
+ description:
1310
+ "Capture a screenshot of the tester's current browser tab.",
1311
+ },
1312
+ ];
1313
+ if (pendingQuestions.length > 0) {
1314
+ responseData.pendingQuestions = pendingQuestions.map((q) => ({
1315
+ questionId: q.questionId,
1316
+ question: q.question,
1317
+ waitingSince: Math.round((Date.now() - q.sentAt) / 1000) + "s ago",
1318
+ }));
1319
+ }
1320
+ } else {
1321
+ // Session ended — include full interaction log and voice transcriptions
1322
+ if (liveInteractions.length > 0 || liveEvents.some((e) => e.eventType === "voice-transcription")) {
1323
+ responseData.sessionEndedNote =
1324
+ "Session has ended. The interactionLog and voiceMarkerTranscriptions below contain all agent-tester exchanges from this session. " +
1325
+ "Include these in your analysis — they represent collaborative context between agent and tester. " +
1326
+ "You can also call get_live_session_summary for the complete summary.";
1327
+ responseData.interactionLog = liveInteractions;
1328
+ // Include all voice marker transcriptions
1329
+ responseData.voiceMarkerTranscriptions = liveEvents
1330
+ .filter((e) => e.eventType === "voice-transcription" && e.data?.transcription)
1331
+ .map((e) => ({
1332
+ timestamp: e.ts,
1333
+ markerId: e.data.markerId,
1334
+ transcription: e.data.transcription,
1335
+ }));
1336
+ }
1337
+ }
1338
+
1339
+ // Always include interaction count so the agent knows exchanges happened
1340
+ if (liveInteractions.length > 0) {
1341
+ responseData.interactionCount = liveInteractions.length;
1342
+ }
1343
+
1175
1344
  return {
1176
1345
  content: [
1177
1346
  {
1178
1347
  type: "text",
1179
- text: JSON.stringify(
1180
- {
1181
- sessionActive: liveSessionActive,
1182
- session: liveSessionMeta,
1183
- events,
1184
- latestSeq,
1185
- eventCount: events.length,
1186
- ...(liveSessionActive
1187
- ? {
1188
- availableActions: [
1189
- {
1190
- tool: "ask_tester_question",
1191
- description:
1192
- "Ask the tester a short question (max 200 chars). They respond with voice + highlights.",
1193
- },
1194
- {
1195
- tool: "get_live_screenshot",
1196
- description:
1197
- "Capture a screenshot of the tester's current browser tab.",
1198
- },
1199
- ],
1200
- }
1201
- : {}),
1202
- },
1203
- null,
1204
- 2,
1205
- ),
1348
+ text: JSON.stringify(responseData, null, 2),
1206
1349
  },
1207
1350
  ],
1208
1351
  };
@@ -1248,6 +1391,17 @@ server.tool(
1248
1391
  pendingQuestions.shift();
1249
1392
  }
1250
1393
 
1394
+ // Log the interaction
1395
+ liveInteractions.push({
1396
+ type: "agent-question",
1397
+ questionId,
1398
+ question,
1399
+ timestamp: Date.now(),
1400
+ });
1401
+
1402
+ // Push a live event so the polling loop sees it
1403
+ pushLiveEvent("agent-question-sent", { questionId, question });
1404
+
1251
1405
  return {
1252
1406
  content: [
1253
1407
  {
@@ -1260,7 +1414,9 @@ server.tool(
1260
1414
  "- Voice marker: Alt/Option + Shift + M (start/stop recording)",
1261
1415
  "- Highlight captures: Alt/Option + Shift + S (draw on screen)",
1262
1416
  "",
1263
- "Use `get_tester_response` with this question ID to check for their answer.",
1417
+ "IMPORTANT: Keep polling watch_live_session you will see a 'question-response-received' event when they respond.",
1418
+ "Then use `get_tester_response` with this question ID to retrieve their full answer.",
1419
+ "The tester may take 10-30 seconds to record their voice response. Be patient.",
1264
1420
  ].join("\n"),
1265
1421
  },
1266
1422
  ],
@@ -1272,7 +1428,11 @@ server.tool(
1272
1428
  server.tool(
1273
1429
  "get_tester_response",
1274
1430
  "Check for the tester's response to a question asked during live shadowing. " +
1275
- "Returns the tester's screenshot, highlight captures, and voice note availability.",
1431
+ "Returns the tester's screenshot, highlight captures, and transcribed voice note.\n\n" +
1432
+ "IMPORTANT: The tester needs time to record their answer (10-60 seconds). " +
1433
+ "Instead of calling this tool repeatedly, prefer polling watch_live_session — " +
1434
+ "you will see a 'question-response-received' event when the response arrives. " +
1435
+ "Then call this tool once to get the full response with transcription and images.",
1276
1436
  {
1277
1437
  question_id: z.string().describe("The question ID returned by ask_tester_question."),
1278
1438
  },
@@ -1280,11 +1440,22 @@ server.tool(
1280
1440
  const response = questionResponses.get(question_id);
1281
1441
 
1282
1442
  if (!response) {
1443
+ // Check how long we've been waiting
1444
+ const pending = pendingQuestions.find((q) => q.questionId === question_id);
1445
+ const waitingSec = pending ? Math.round((Date.now() - pending.sentAt) / 1000) : null;
1283
1446
  return {
1284
1447
  content: [
1285
1448
  {
1286
1449
  type: "text",
1287
- text: "No response yet. The tester may still be recording their answer. Try again in a few seconds.",
1450
+ text: [
1451
+ "No response yet." +
1452
+ (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1453
+ "The tester may still be recording their voice answer.",
1454
+ "",
1455
+ "TIP: Instead of polling this tool, go back to polling watch_live_session — " +
1456
+ "a 'question-response-received' event will appear when the tester responds. " +
1457
+ "Then call this tool once to retrieve the full answer.",
1458
+ ].join("\n"),
1288
1459
  },
1289
1460
  ],
1290
1461
  };
@@ -1345,20 +1516,34 @@ server.tool(
1345
1516
  }
1346
1517
 
1347
1518
  if (response.voiceBlobDataUrl) {
1348
- const voiceResult = await transcribeLiveVoice(response.voiceBlobDataUrl);
1349
- if (voiceResult.transcription) {
1519
+ // Use pre-transcribed result if available (from eager transcription on response arrival)
1520
+ if (response.transcription) {
1350
1521
  content.push({
1351
1522
  type: "text",
1352
- text: `Voice transcription:\n${voiceResult.transcription}`,
1523
+ text: `Voice transcription:\n${response.transcription}`,
1353
1524
  });
1354
1525
  } else {
1355
- content.push({
1356
- type: "text",
1357
- text: `Voice note is attached but could not be transcribed${voiceResult.error ? ` (${voiceResult.error})` : ""}. Set OPENROUTER_API_KEY or install local Whisper for transcription.`,
1358
- });
1526
+ // Fallback: transcribe now if eager transcription hasn't completed yet
1527
+ const voiceResult = await transcribeLiveVoice(response.voiceBlobDataUrl);
1528
+ if (voiceResult.transcription) {
1529
+ content.push({
1530
+ type: "text",
1531
+ text: `Voice transcription:\n${voiceResult.transcription}`,
1532
+ });
1533
+ } else {
1534
+ content.push({
1535
+ type: "text",
1536
+ text: `Voice note is attached but could not be transcribed${voiceResult.error ? ` (${voiceResult.error})` : ""}. Set OPENROUTER_API_KEY or install local Whisper for transcription.`,
1537
+ });
1538
+ }
1359
1539
  }
1360
1540
  }
1361
1541
 
1542
+ content.push({
1543
+ type: "text",
1544
+ text: "\nIMPORTANT: Resume polling watch_live_session to continue watching the session.",
1545
+ });
1546
+
1362
1547
  // Clean up after retrieval
1363
1548
  questionResponses.delete(question_id);
1364
1549
 
@@ -1420,6 +1605,50 @@ server.tool(
1420
1605
  },
1421
1606
  );
1422
1607
 
1608
+ // Tool: get_live_session_summary
1609
+ server.tool(
1610
+ "get_live_session_summary",
1611
+ "Get a summary of the live shadowing session including all agent-tester interactions. " +
1612
+ "Call this after the session ends (sessionActive becomes false) to get the complete interaction log " +
1613
+ "for incorporating into your analysis. This includes all questions asked, tester responses, " +
1614
+ "voice transcriptions, and voice marker transcriptions from the session.",
1615
+ {},
1616
+ async () => {
1617
+ if (!liveSessionMeta && liveInteractions.length === 0) {
1618
+ return toolError("No live session data available. Start a live shadowing session first.");
1619
+ }
1620
+
1621
+ // Collect voice-transcription events from the event buffer
1622
+ const voiceTranscriptions = liveEvents
1623
+ .filter((e) => e.eventType === "voice-transcription" && e.data?.transcription)
1624
+ .map((e) => ({
1625
+ timestamp: e.ts,
1626
+ markerId: e.data.markerId,
1627
+ transcription: e.data.transcription,
1628
+ }));
1629
+
1630
+ const summary = {
1631
+ session: liveSessionMeta,
1632
+ sessionActive: liveSessionActive,
1633
+ interactionLog: liveInteractions,
1634
+ voiceMarkerTranscriptions: voiceTranscriptions,
1635
+ totalEvents: liveEvents.length,
1636
+ guidance: liveSessionActive
1637
+ ? "Session is still active. Resume polling watch_live_session."
1638
+ : "Session has ended. Use this interaction log and voice transcriptions to enrich your analysis of the exported session package.",
1639
+ };
1640
+
1641
+ return {
1642
+ content: [
1643
+ {
1644
+ type: "text",
1645
+ text: JSON.stringify(summary, null, 2),
1646
+ },
1647
+ ],
1648
+ };
1649
+ },
1650
+ );
1651
+
1423
1652
  // ---------------------------------------------------------------------------
1424
1653
  // Startup
1425
1654
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracegist-mcp-bridge",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Local-first MCP bridge for reading and transcribing TraceGist package zips.",
5
5
  "type": "module",
6
6
  "bin": {