tracegist-mcp-bridge 0.2.6 → 0.2.7

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
 
@@ -1039,6 +1047,7 @@ function handleExtensionMessage(msg) {
1039
1047
  liveEvents.length = 0;
1040
1048
  pendingQuestions.length = 0;
1041
1049
  questionResponses.clear();
1050
+ liveInteractions.length = 0;
1042
1051
  console.error(`[${BRIDGE_NAME}] Live shadow: session started (${msg.sessionId})`);
1043
1052
  // Notify MCP clients that resources changed
1044
1053
  try {
@@ -1072,6 +1081,14 @@ function handleExtensionMessage(msg) {
1072
1081
  });
1073
1082
  const rIdx = pendingQuestions.findIndex((q) => q.questionId === msg.questionId);
1074
1083
  if (rIdx >= 0) pendingQuestions.splice(rIdx, 1);
1084
+ // Push a live event so polling agents see the response immediately
1085
+ pushLiveEvent("question-response-received", {
1086
+ questionId: msg.questionId,
1087
+ hasVoice: !!msg.voiceBlobDataUrl,
1088
+ hasScreenshot: !!msg.screenshot,
1089
+ highlightCount: msg.highlightCaptures?.length || 0,
1090
+ hint: "Use get_tester_response to retrieve the full response with transcription and images.",
1091
+ });
1075
1092
  break;
1076
1093
  }
1077
1094
 
@@ -1079,6 +1096,14 @@ function handleExtensionMessage(msg) {
1079
1096
  questionResponses.set(msg.questionId, { dismissed: true });
1080
1097
  const dIdx = pendingQuestions.findIndex((q) => q.questionId === msg.questionId);
1081
1098
  if (dIdx >= 0) pendingQuestions.splice(dIdx, 1);
1099
+ // Log dismissal in interactions
1100
+ liveInteractions.push({
1101
+ type: "question-dismissed",
1102
+ questionId: msg.questionId,
1103
+ timestamp: Date.now(),
1104
+ dismissed: true,
1105
+ });
1106
+ pushLiveEvent("question-dismissed", { questionId: msg.questionId });
1082
1107
  break;
1083
1108
  }
1084
1109
 
@@ -1099,6 +1124,13 @@ function handleExtensionMessage(msg) {
1099
1124
  transcription: result.transcription || null,
1100
1125
  error: result.error || undefined,
1101
1126
  });
1127
+ // Log voice marker in interactions for the final artifact
1128
+ liveInteractions.push({
1129
+ type: "voice-marker",
1130
+ questionId: markerId,
1131
+ timestamp: Date.now(),
1132
+ transcription: result.transcription || null,
1133
+ });
1102
1134
  })
1103
1135
  .catch((err) => {
1104
1136
  console.error(`[${BRIDGE_NAME}] Live voice transcription failed:`, err);
@@ -1151,9 +1183,14 @@ server.resource(
1151
1183
  // Tool: watch_live_session
1152
1184
  server.tool(
1153
1185
  "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.",
1186
+ "Watch a live TraceGist shadowing session in real-time. Returns buffered events since the given sequence number.\n\n" +
1187
+ "IMPORTANT POLLING BEHAVIOR:\n" +
1188
+ "- You MUST keep calling this tool every 2-4 seconds in a loop until sessionActive becomes false.\n" +
1189
+ "- Do NOT stop polling just because there are no new events — the tester is still active.\n" +
1190
+ "- When you see 'voice-transcription' events, the tester recorded a voice marker — read the transcription and react to it.\n" +
1191
+ "- When you see 'question-response-received' events, use get_tester_response to retrieve the full answer.\n" +
1192
+ "- The tester may set voice markers at any time to communicate with you — keep watching for them.\n" +
1193
+ "- Only stop polling when sessionActive is false (the recording ended).",
1157
1194
  {
1158
1195
  since_seq: z
1159
1196
  .number()
@@ -1172,37 +1209,56 @@ server.tool(
1172
1209
 
1173
1210
  const latestSeq = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1].seq : 0;
1174
1211
 
1212
+ const responseData = {
1213
+ sessionActive: liveSessionActive,
1214
+ session: liveSessionMeta,
1215
+ events,
1216
+ latestSeq,
1217
+ eventCount: events.length,
1218
+ };
1219
+
1220
+ if (liveSessionActive) {
1221
+ responseData.pollingGuidance =
1222
+ "KEEP POLLING every 2-4 seconds. Do NOT stop — the tester is still active. " +
1223
+ "Watch for 'voice-transcription' events (tester voice markers) and 'question-response-received' events. " +
1224
+ "React to voice markers by acknowledging them or asking follow-up questions.";
1225
+ responseData.availableActions = [
1226
+ {
1227
+ tool: "ask_tester_question",
1228
+ description:
1229
+ "Ask the tester a short question (max 200 chars). They respond with voice + highlights.",
1230
+ },
1231
+ {
1232
+ tool: "get_live_screenshot",
1233
+ description:
1234
+ "Capture a screenshot of the tester's current browser tab.",
1235
+ },
1236
+ ];
1237
+ if (pendingQuestions.length > 0) {
1238
+ responseData.pendingQuestions = pendingQuestions.map((q) => ({
1239
+ questionId: q.questionId,
1240
+ question: q.question,
1241
+ waitingSince: Math.round((Date.now() - q.sentAt) / 1000) + "s ago",
1242
+ }));
1243
+ }
1244
+ } else if (liveInteractions.length > 0) {
1245
+ // Session ended — include full interaction log for the agent to incorporate
1246
+ responseData.sessionEndedNote =
1247
+ "Session has ended. The interactionLog below contains all agent-tester exchanges from this session. " +
1248
+ "Include these in your analysis — they represent collaborative context between agent and tester.";
1249
+ responseData.interactionLog = liveInteractions;
1250
+ }
1251
+
1252
+ // Always include interaction count so the agent knows exchanges happened
1253
+ if (liveInteractions.length > 0) {
1254
+ responseData.interactionCount = liveInteractions.length;
1255
+ }
1256
+
1175
1257
  return {
1176
1258
  content: [
1177
1259
  {
1178
1260
  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
- ),
1261
+ text: JSON.stringify(responseData, null, 2),
1206
1262
  },
1207
1263
  ],
1208
1264
  };
@@ -1248,6 +1304,17 @@ server.tool(
1248
1304
  pendingQuestions.shift();
1249
1305
  }
1250
1306
 
1307
+ // Log the interaction
1308
+ liveInteractions.push({
1309
+ type: "agent-question",
1310
+ questionId,
1311
+ question,
1312
+ timestamp: Date.now(),
1313
+ });
1314
+
1315
+ // Push a live event so the polling loop sees it
1316
+ pushLiveEvent("agent-question-sent", { questionId, question });
1317
+
1251
1318
  return {
1252
1319
  content: [
1253
1320
  {
@@ -1260,7 +1327,9 @@ server.tool(
1260
1327
  "- Voice marker: Alt/Option + Shift + M (start/stop recording)",
1261
1328
  "- Highlight captures: Alt/Option + Shift + S (draw on screen)",
1262
1329
  "",
1263
- "Use `get_tester_response` with this question ID to check for their answer.",
1330
+ "IMPORTANT: Keep polling watch_live_session you will see a 'question-response-received' event when they respond.",
1331
+ "Then use `get_tester_response` with this question ID to retrieve their full answer.",
1332
+ "The tester may take 10-30 seconds to record their voice response. Be patient.",
1264
1333
  ].join("\n"),
1265
1334
  },
1266
1335
  ],
@@ -1272,7 +1341,11 @@ server.tool(
1272
1341
  server.tool(
1273
1342
  "get_tester_response",
1274
1343
  "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.",
1344
+ "Returns the tester's screenshot, highlight captures, and transcribed voice note.\n\n" +
1345
+ "IMPORTANT: The tester needs time to record their answer (10-60 seconds). " +
1346
+ "Instead of calling this tool repeatedly, prefer polling watch_live_session — " +
1347
+ "you will see a 'question-response-received' event when the response arrives. " +
1348
+ "Then call this tool once to get the full response with transcription and images.",
1276
1349
  {
1277
1350
  question_id: z.string().describe("The question ID returned by ask_tester_question."),
1278
1351
  },
@@ -1280,11 +1353,22 @@ server.tool(
1280
1353
  const response = questionResponses.get(question_id);
1281
1354
 
1282
1355
  if (!response) {
1356
+ // Check how long we've been waiting
1357
+ const pending = pendingQuestions.find((q) => q.questionId === question_id);
1358
+ const waitingSec = pending ? Math.round((Date.now() - pending.sentAt) / 1000) : null;
1283
1359
  return {
1284
1360
  content: [
1285
1361
  {
1286
1362
  type: "text",
1287
- text: "No response yet. The tester may still be recording their answer. Try again in a few seconds.",
1363
+ text: [
1364
+ "No response yet." +
1365
+ (waitingSec !== null ? ` Waiting for ${waitingSec}s.` : ""),
1366
+ "The tester may still be recording their voice answer.",
1367
+ "",
1368
+ "TIP: Instead of polling this tool, go back to polling watch_live_session — " +
1369
+ "a 'question-response-received' event will appear when the tester responds. " +
1370
+ "Then call this tool once to retrieve the full answer.",
1371
+ ].join("\n"),
1288
1372
  },
1289
1373
  ],
1290
1374
  };
@@ -1344,9 +1428,11 @@ server.tool(
1344
1428
  }
1345
1429
  }
1346
1430
 
1431
+ let transcriptionText = null;
1347
1432
  if (response.voiceBlobDataUrl) {
1348
1433
  const voiceResult = await transcribeLiveVoice(response.voiceBlobDataUrl);
1349
1434
  if (voiceResult.transcription) {
1435
+ transcriptionText = voiceResult.transcription;
1350
1436
  content.push({
1351
1437
  type: "text",
1352
1438
  text: `Voice transcription:\n${voiceResult.transcription}`,
@@ -1359,6 +1445,21 @@ server.tool(
1359
1445
  }
1360
1446
  }
1361
1447
 
1448
+ // Log the interaction for the session history
1449
+ liveInteractions.push({
1450
+ type: "tester-response",
1451
+ questionId: question_id,
1452
+ timestamp: Date.now(),
1453
+ transcription: transcriptionText,
1454
+ screenshot: !!response.screenshot,
1455
+ highlightCount: response.highlightCaptures?.length || 0,
1456
+ });
1457
+
1458
+ content.push({
1459
+ type: "text",
1460
+ text: "\nIMPORTANT: Resume polling watch_live_session to continue watching the session.",
1461
+ });
1462
+
1362
1463
  // Clean up after retrieval
1363
1464
  questionResponses.delete(question_id);
1364
1465
 
@@ -1420,6 +1521,50 @@ server.tool(
1420
1521
  },
1421
1522
  );
1422
1523
 
1524
+ // Tool: get_live_session_summary
1525
+ server.tool(
1526
+ "get_live_session_summary",
1527
+ "Get a summary of the live shadowing session including all agent-tester interactions. " +
1528
+ "Call this after the session ends (sessionActive becomes false) to get the complete interaction log " +
1529
+ "for incorporating into your analysis. This includes all questions asked, tester responses, " +
1530
+ "voice transcriptions, and voice marker transcriptions from the session.",
1531
+ {},
1532
+ async () => {
1533
+ if (!liveSessionMeta && liveInteractions.length === 0) {
1534
+ return toolError("No live session data available. Start a live shadowing session first.");
1535
+ }
1536
+
1537
+ // Collect voice-transcription events from the event buffer
1538
+ const voiceTranscriptions = liveEvents
1539
+ .filter((e) => e.eventType === "voice-transcription" && e.data?.transcription)
1540
+ .map((e) => ({
1541
+ timestamp: e.ts,
1542
+ markerId: e.data.markerId,
1543
+ transcription: e.data.transcription,
1544
+ }));
1545
+
1546
+ const summary = {
1547
+ session: liveSessionMeta,
1548
+ sessionActive: liveSessionActive,
1549
+ interactionLog: liveInteractions,
1550
+ voiceMarkerTranscriptions: voiceTranscriptions,
1551
+ totalEvents: liveEvents.length,
1552
+ guidance: liveSessionActive
1553
+ ? "Session is still active. Resume polling watch_live_session."
1554
+ : "Session has ended. Use this interaction log and voice transcriptions to enrich your analysis of the exported session package.",
1555
+ };
1556
+
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: "text",
1561
+ text: JSON.stringify(summary, null, 2),
1562
+ },
1563
+ ],
1564
+ };
1565
+ },
1566
+ );
1567
+
1423
1568
  // ---------------------------------------------------------------------------
1424
1569
  // Startup
1425
1570
  // ---------------------------------------------------------------------------
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.7",
4
4
  "description": "Local-first MCP bridge for reading and transcribing TraceGist package zips.",
5
5
  "type": "module",
6
6
  "bin": {