@obtoai/agent-bridge 0.1.0-beta.15 → 0.1.0-beta.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obtoai/agent-bridge",
3
- "version": "0.1.0-beta.15",
3
+ "version": "0.1.0-beta.17",
4
4
  "description": "Local consumer for the OBTO Agent Bridge. Receives bridge events over SSE and drives a coding agent (Claude Code or OpenAI Codex) on your machine.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "OBTO Inc.",
@@ -53,6 +53,78 @@ const readTail = (filePath, maxBytes = TAIL_READ_BYTES) => {
53
53
  }
54
54
  };
55
55
 
56
+ // Pull the last N user/assistant *logical turns* from the tail, oldest-first.
57
+ // Phase 6.2.3 — beta.17 fix: Claude Code streams a single assistant reply as
58
+ // MULTIPLE JSONL lines (text → tool_use → tool_result → text → …), each its
59
+ // own JSONL record. The earlier extractor took one line per turn and
60
+ // fragmented responses into single-shard previews. Now we walk forward,
61
+ // parse every text-bearing user/assistant line, and **coalesce consecutive
62
+ // same-role lines into one logical turn** before slicing the last N. Result:
63
+ // a real conversation, not a single mid-sentence excerpt.
64
+ const RECENT_MESSAGE_BODY_MAX = 3000;
65
+ const RECENT_TURN_COUNT = 10;
66
+
67
+ const extractRecentMessages = (jsonlTail, n = RECENT_TURN_COUNT) => {
68
+ if (!jsonlTail) return [];
69
+ const lines = jsonlTail.split(/\r?\n/);
70
+ const parsed = [];
71
+ for (const lineRaw of lines) {
72
+ const line = lineRaw.trim();
73
+ if (!line) continue;
74
+ let obj;
75
+ try { obj = JSON.parse(line); } catch (_) { continue; }
76
+ let role = null, raw = null;
77
+ if (obj && obj.message && (obj.message.role || obj.type)) {
78
+ role = obj.message.role || (obj.type === 'user' ? 'user' : 'assistant');
79
+ raw = obj.message.content;
80
+ } else if (obj && obj.role && (obj.content || obj.text)) {
81
+ role = obj.role;
82
+ raw = obj.content != null ? obj.content : obj.text;
83
+ }
84
+ if (role !== 'user' && role !== 'assistant') continue;
85
+ if (raw == null) continue;
86
+ let text = '';
87
+ if (typeof raw === 'string') text = raw;
88
+ else if (Array.isArray(raw)) {
89
+ text = raw
90
+ .filter((p) => p && (p.type === 'text' || typeof p.text === 'string'))
91
+ .map((p) => String(p.text || ''))
92
+ .join(' ');
93
+ }
94
+ text = text.replace(/\s+/g, ' ').trim();
95
+ if (!text) continue; // skip pure tool_use / tool_result lines (no text body)
96
+ parsed.push({ role, text, ts: obj.timestamp || null });
97
+ }
98
+
99
+ // Coalesce consecutive same-role lines (one logical turn split across JSONL
100
+ // records). This is the actual fix — without it, assistant turns with mid-
101
+ // reply tool calls fragment into the first text shard only.
102
+ const coalesced = [];
103
+ for (const p of parsed) {
104
+ const last = coalesced[coalesced.length - 1];
105
+ if (last && last.role === p.role) {
106
+ last.text = (last.text + ' ' + p.text).replace(/\s+/g, ' ').trim();
107
+ last.ts = p.ts || last.ts;
108
+ } else {
109
+ coalesced.push({ role: p.role, text: p.text, ts: p.ts });
110
+ }
111
+ }
112
+
113
+ // Filter user turns that are pure platform-injection noise. Assistant turns
114
+ // are never filtered — they're always real Claude/Codex output.
115
+ const filtered = coalesced.filter((m) => {
116
+ if (m.role !== 'user') return true;
117
+ return !isInjectionMessage(m.text);
118
+ });
119
+
120
+ const sliced = filtered.slice(-n);
121
+ return sliced.map((m) => ({
122
+ role: m.role,
123
+ body: m.text.length > RECENT_MESSAGE_BODY_MAX ? m.text.slice(0, RECENT_MESSAGE_BODY_MAX) : m.text,
124
+ ts: m.ts,
125
+ }));
126
+ };
127
+
56
128
  // Claude Code writes an LLM-generated title as a `type: "ai-title"` JSONL
57
129
  // record near the end of each session file (this is the same title VSCode's
58
130
  // session list shows — "Analyze MongoDB MCP server architecture" style).
@@ -251,12 +323,14 @@ const scanClaude = () => {
251
323
  // have an ai-title record yet.
252
324
  let title = extractAiTitleFromTail(tail);
253
325
  if (!title) title = extractTitleFromFile(filePath);
326
+ const recentMessages = extractRecentMessages(tail);
254
327
  out.push({
255
328
  source: 'claude',
256
329
  sessionId,
257
330
  projectDir: entry, // raw encoded form
258
331
  projectName: decodeClaudeProjectDir(entry), // best-effort decoded
259
- title: title, // first-user-message slice
332
+ title: title, // ai-title (Claude) or first-user-message fallback
333
+ recentMessages: recentMessages, // last ≤5 user/assistant turns for adopt-time history
260
334
  lastActivityAt: stat.mtimeMs,
261
335
  lastMessagePreview: lastMsg ? lastMsg.preview : '',
262
336
  lastMessageAuthor: lastMsg ? lastMsg.author : null,
@@ -324,16 +398,15 @@ const scanCodex = () => {
324
398
 
325
399
  const tail = readTail(filePath);
326
400
  const lastMsg = extractLastMessage(tail);
327
- // Codex rollouts don't carry an ai-title record (as of this writing),
328
- // so we just use the first-user-message fallback. If they add one
329
- // later, swap in extractAiTitleFromTail like the claude path does.
330
401
  const title = extractTitleFromFile(filePath);
402
+ const recentMessages = extractRecentMessages(tail);
331
403
  out.push({
332
404
  source: 'codex',
333
405
  sessionId,
334
406
  projectDir: projectDir || `${y}/${m}/${d}`,
335
407
  projectName: projectDir || null,
336
408
  title: title,
409
+ recentMessages: recentMessages,
337
410
  lastActivityAt: stat.mtimeMs,
338
411
  lastMessagePreview: lastMsg ? lastMsg.preview : '',
339
412
  lastMessageAuthor: lastMsg ? lastMsg.author : null,