cursorconnect 0.1.7 → 0.1.9

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.
Files changed (44) hide show
  1. package/bridge-runtime/.env.example +7 -1
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +27 -22
  4. package/bridge-runtime/dist/agent-completion-push.js +242 -122
  5. package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
  6. package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
  7. package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
  8. package/bridge-runtime/dist/chat-display-store.js +99 -21
  9. package/bridge-runtime/dist/chat-display.d.ts +36 -0
  10. package/bridge-runtime/dist/chat-display.js +287 -24
  11. package/bridge-runtime/dist/chat-sync.d.ts +3 -1
  12. package/bridge-runtime/dist/chat-sync.js +20 -0
  13. package/bridge-runtime/dist/config.js +2 -0
  14. package/bridge-runtime/dist/connector-client-version.js +1 -1
  15. package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
  16. package/bridge-runtime/dist/debug-chats-page.js +148 -26
  17. package/bridge-runtime/dist/dom-transcript-store.d.ts +3 -1
  18. package/bridge-runtime/dist/dom-transcript-store.js +18 -3
  19. package/bridge-runtime/dist/extract-page.js +5 -4
  20. package/bridge-runtime/dist/index.js +9 -0
  21. package/bridge-runtime/dist/keep-awake.d.ts +5 -0
  22. package/bridge-runtime/dist/keep-awake.js +48 -0
  23. package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
  24. package/bridge-runtime/dist/lenta-capture.js +146 -0
  25. package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
  26. package/bridge-runtime/dist/lenta-debug.js +221 -0
  27. package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
  28. package/bridge-runtime/dist/lenta-delivery.js +10 -0
  29. package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
  30. package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
  31. package/bridge-runtime/dist/message-filter.d.ts +5 -0
  32. package/bridge-runtime/dist/message-filter.js +4 -0
  33. package/bridge-runtime/dist/relay-upstream.d.ts +3 -0
  34. package/bridge-runtime/dist/relay-upstream.js +21 -0
  35. package/bridge-runtime/dist/relay.d.ts +47 -3
  36. package/bridge-runtime/dist/relay.js +667 -96
  37. package/bridge-runtime/dist/types.d.ts +13 -4
  38. package/dist/bridge-build.js +50 -0
  39. package/dist/index.js +9 -6
  40. package/dist/launch.js +5 -1
  41. package/dist/run-service.js +10 -4
  42. package/dist/startup-check.js +6 -0
  43. package/package.json +1 -1
  44. package/version-policy.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { mkdirSync } from 'fs';
1
2
  import express from 'express';
2
3
  import { createServer } from 'http';
3
4
  import { randomBytes, timingSafeEqual } from 'crypto';
@@ -6,9 +7,9 @@ import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
6
7
  import { Server as SocketServer } from 'socket.io';
7
8
  import { resolveJsonlFilePath } from './agent-title-match.js';
8
9
  import { ChatDisplayStore } from './chat-display-store.js';
9
- import { AGENT_HISTORY_DEFAULT_LIMIT } from './history-limit.js';
10
10
  import { resolveHistoryLimit } from './history-request.js';
11
- import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
11
+ import { assertMonotonicLenta, filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
12
+ import { isKeepAwakeActive } from './keep-awake.js';
12
13
  function sleepMs(ms) {
13
14
  return new Promise((resolve) => setTimeout(resolve, ms));
14
15
  }
@@ -20,7 +21,12 @@ import { RelayUpstream } from './relay-upstream.js';
20
21
  import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
21
22
  import { DEBUG_CHATS_PAGE_HTML } from './debug-chats-page.js';
22
23
  import { isChatHistoryFromJsonl } from './chat-history-mode.js';
24
+ import { isAgentGenerating, isChatSyncedWithCursor, isComposerUuid, resolveCursorActiveComposerId, } from './chat-sync.js';
23
25
  import { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
26
+ import { checkMonotonicOrder, checkOverlayAfterArchive, checkOverlayDuplicatesArchive, checkPayloadCompose, checkStoreCompose, checkSyncGate, compareDomTailToLenta, previewRow, } from './lenta-debug.js';
27
+ import { getLastLentaEmit, getLentaSeqJournal, getLentaSeqLogPath, messagesFingerprint, recordLentaSeqEmit, } from './lenta-seq-journal.js';
28
+ import { isLentaDeliveryPending } from './lenta-delivery.js';
29
+ import { captureLentaIfEnabled, captureSessionDir, listCaptureSteps, readCaptureConfig, writeCaptureConfig, clearCaptureConfig, } from './lenta-capture.js';
24
30
  export class Relay {
25
31
  stateManager;
26
32
  commandExecutor;
@@ -44,6 +50,16 @@ export class Relay {
44
50
  chatDisplay = new ChatDisplayStore();
45
51
  /** Raw JSONL row count last sent per agent (live `append` emits). */
46
52
  lastEmittedJsonlRows = new Map();
53
+ /** Last display tail pushed to app — re-emit when user/assistant tail changes. */
54
+ lastEmittedLentaSig = new Map();
55
+ /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
56
+ lastEmittedHistLen = new Map();
57
+ lentaPendingSince = new Map();
58
+ static LENTA_PENDING_FORCE_MS = 12_000;
59
+ /** Socket `agent:messages` coalesce — store ingest is immediate (see ingestDomOverlayFromState). */
60
+ static DOM_OVERLAY_EMIT_MS = parseInt(process.env.DOM_OVERLAY_EMIT_MS ?? '50', 10);
61
+ domOverlayTimer = null;
62
+ cachedLentaPendingByAgent = {};
47
63
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
48
64
  this.stateManager = stateManager;
49
65
  this.commandExecutor = commandExecutor;
@@ -67,7 +83,15 @@ export class Relay {
67
83
  this.upstream = new RelayUpstream(this.config, (event, ...args) => {
68
84
  this.handleRemoteClientEvent(event, ...args);
69
85
  }, () => this.flushPendingPushPayloads());
70
- this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState());
86
+ this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState(), {
87
+ readiness: {
88
+ isSubscribed: (agentId) => this.jsonlIndex.getSubscribedAgents().has(agentId),
89
+ getSubscribeTitle: (agentId) => this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
90
+ getDisplayMessages: (agentId) => this.chatDisplay.getDisplayMessages(agentId),
91
+ getJsonlHistory: (agentId) => this.chatDisplay.getJsonlHistory(agentId),
92
+ },
93
+ requestContentSync: (agentId, state) => this.requestAgentCompletionContentSync(agentId, state),
94
+ });
71
95
  this.upstream.connect();
72
96
  }
73
97
  }
@@ -75,6 +99,7 @@ export class Relay {
75
99
  emitAgentCompletedPush(payload) {
76
100
  if (this.upstream?.connected) {
77
101
  this.upstream.emit('push:agent-completed', payload);
102
+ console.log(`[agent-completion-push] upstream emit agentId=${payload.agentId.slice(0, 12)} title=${payload.chatTitle.slice(0, 40)}`);
78
103
  return;
79
104
  }
80
105
  this.pendingPushPayloads.push(payload);
@@ -83,15 +108,22 @@ export class Relay {
83
108
  flushPendingPushPayloads() {
84
109
  if (!this.upstream?.connected || !this.pendingPushPayloads.length)
85
110
  return;
86
- const batch = this.pendingPushPayloads.splice(0);
87
- for (const payload of batch) {
111
+ const byAgent = new Map();
112
+ for (const payload of this.pendingPushPayloads.splice(0)) {
113
+ byAgent.set(payload.agentId, payload);
114
+ }
115
+ for (const payload of byAgent.values()) {
88
116
  this.upstream.emit('push:agent-completed', payload);
89
117
  }
90
- console.log(`[agent-completion-push] flushed ${batch.length} queued push(es) to relay`);
118
+ console.log(`[agent-completion-push] flushed ${byAgent.size} queued push(es) to relay`);
91
119
  }
92
120
  observeAgentCompletionForPush() {
93
121
  this.agentCompletionPush?.observe(this.stateManager.getState());
94
122
  }
123
+ requestAgentCompletionContentSync(agentId, state) {
124
+ void this.ensureJsonlBaselineForAgent(agentId, state);
125
+ this.flushLentaDelivery(agentId, 'push_ready');
126
+ }
95
127
  listen() {
96
128
  return new Promise((resolve) => {
97
129
  this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
@@ -103,23 +135,33 @@ export class Relay {
103
135
  get authEnabled() {
104
136
  return this.config.webappPassword.length > 0;
105
137
  }
138
+ lentaAgentIds(state) {
139
+ const subscribed = [...this.jsonlIndex.getSubscribedAgents().keys()];
140
+ return new Set([
141
+ ...subscribed,
142
+ ...(state.activeComposerId ? [state.activeComposerId] : []),
143
+ ]);
144
+ }
106
145
  /** Read-only view of in-memory bridge state (no writes to stores). */
107
146
  readOnlyChatSnapshot() {
108
147
  const state = this.stateManager.getState();
109
148
  const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
110
149
  const displayCache = {};
111
- const cacheIds = new Set([
112
- ...subscribed.map((s) => s.agentId),
113
- ...(state.activeComposerId ? [state.activeComposerId] : []),
114
- ]);
115
- const domTranscript = {};
116
- for (const agentId of cacheIds) {
117
- const dom = this.chatDisplay.getDomTranscript(agentId);
118
- if (dom.length)
119
- domTranscript[agentId] = dom;
120
- const jsonl = this.chatDisplay.getJsonlHistory(agentId);
121
- if (jsonl.length)
122
- displayCache[agentId] = jsonl;
150
+ const domOverlay = {};
151
+ const displayMessages = {};
152
+ const domRaw = {};
153
+ const lentaByAgent = {};
154
+ for (const agentId of this.lentaAgentIds(state)) {
155
+ const lenta = this.chatDisplay.getLentaDebug(agentId);
156
+ lentaByAgent[agentId] = lenta;
157
+ if (lenta.jsonlHistory.length)
158
+ displayCache[agentId] = lenta.jsonlHistory;
159
+ if (lenta.domOverlay.length)
160
+ domOverlay[agentId] = lenta.domOverlay;
161
+ if (lenta.messages.length)
162
+ displayMessages[agentId] = lenta.messages;
163
+ if (lenta.domRaw.length)
164
+ domRaw[agentId] = lenta.domRaw;
123
165
  }
124
166
  return {
125
167
  at: Date.now(),
@@ -129,6 +171,7 @@ export class Relay {
129
171
  activeChatTitle: state.activeChatTitle,
130
172
  updatedAt: state.updatedAt,
131
173
  lastError: state.lastError,
174
+ agentWorking: state.agentWorking,
132
175
  tabs: state.tabs,
133
176
  composerIdByTitle: state.composerIdByTitle,
134
177
  domMessageCount: state.messages.length,
@@ -139,9 +182,83 @@ export class Relay {
139
182
  ringSize: this.messageDebugStore.list().length,
140
183
  },
141
184
  subscribed,
185
+ lentaByAgent,
142
186
  displayCache,
187
+ domOverlay,
188
+ displayMessages,
189
+ domRaw,
190
+ /** @deprecated use `domOverlay` */
191
+ domTranscript: domOverlay,
143
192
  chatHistoryJsonl: isChatHistoryFromJsonl(),
144
- domTranscript,
193
+ };
194
+ }
195
+ async buildLentaDebugReport(agentId, title, opts) {
196
+ const state = this.stateManager.getState();
197
+ const subMeta = this.jsonlIndex.getSubscribedAgents().get(agentId);
198
+ const subTitle = title ?? subMeta?.title;
199
+ const synced = isChatSyncedWithCursor(agentId, subTitle, state);
200
+ if (opts?.reloadJsonl) {
201
+ const history = await this.jsonlIndex.loadHistory(agentId, {
202
+ title: subTitle,
203
+ composerIdByTitle: state.composerIdByTitle,
204
+ });
205
+ this.chatDisplay.setJsonlBaseline(agentId, history.messages);
206
+ }
207
+ this.syncGeneratingFlags(state);
208
+ if (state.messages?.length)
209
+ this.ingestDomOverlayFromState(state);
210
+ const lenta = this.chatDisplay.getLentaDebug(agentId);
211
+ const snap = this.agentMessagesSnapshot(agentId);
212
+ const issues = [
213
+ ...checkStoreCompose(lenta.jsonlHistory, lenta.domOverlay, lenta.messages),
214
+ ...checkPayloadCompose(snap.messages, snap.historyMessages, snap.liveMessages),
215
+ ...checkMonotonicOrder(lenta.messages, 'store'),
216
+ ...checkOverlayAfterArchive(lenta.jsonlHistory, lenta.domOverlay),
217
+ ...checkOverlayDuplicatesArchive(lenta.jsonlHistory, lenta.domOverlay),
218
+ ...checkSyncGate(synced, lenta.domOverlay, lenta.domRaw.length),
219
+ ];
220
+ const activeId = state.activeComposerId;
221
+ const domViewport = activeId && activeId !== agentId
222
+ ? []
223
+ : state.messages.map((m) => previewRow(m, 'dom-viewport'));
224
+ const domNote = activeId && activeId !== agentId
225
+ ? `DOM viewport is active ${activeId.slice(0, 8)}, not ${agentId.slice(0, 8)}`
226
+ : null;
227
+ const domCmp = synced
228
+ ? compareDomTailToLenta(domViewport, lenta.messages.map((m) => previewRow(m, 'lenta')), 12, {
229
+ hasDomOverlay: lenta.domOverlay.length > 0,
230
+ hasJsonlArchive: lenta.jsonlHistory.length > 0,
231
+ agentWorking: state.agentWorking,
232
+ })
233
+ : { issues: [], domTail: [], lentaTail: [] };
234
+ issues.push(...domCmp.issues);
235
+ return {
236
+ at: new Date().toISOString(),
237
+ agentId,
238
+ synced,
239
+ domNote,
240
+ health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
241
+ activeComposerId: activeId,
242
+ activeChatTitle: state.activeChatTitle,
243
+ agentWorking: state.agentWorking,
244
+ agentGenerating: lenta.agentGenerating,
245
+ jsonlRowCount: lenta.jsonlRowCount,
246
+ counts: {
247
+ jsonl: lenta.jsonlHistory.length,
248
+ domOverlay: lenta.domOverlay.length,
249
+ domRaw: lenta.domRaw.length,
250
+ messages: lenta.messages.length,
251
+ domViewport: domViewport.length,
252
+ },
253
+ lenta,
254
+ emitSnapshot: snap,
255
+ issues,
256
+ match: issues.length === 0,
257
+ domTail: domCmp.domTail,
258
+ lentaTail: domCmp.lentaTail,
259
+ seqJournal: getLentaSeqJournal(agentId).slice(-24),
260
+ seqLogFile: getLentaSeqLogPath(),
261
+ currentSeq: this.historySeqByAgent.get(agentId) ?? 0,
145
262
  };
146
263
  }
147
264
  /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
@@ -163,47 +280,262 @@ export class Relay {
163
280
  this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
164
281
  }
165
282
  }
166
- /** Live JSONL: push display deltas to app; `totalMessages` = raw `.jsonl` line count. */
283
+ /** JSONL baseline + DOM tail (`liveMessages`); `messages` = append-only compose. */
284
+ lentaSnapshot(agentId, totalMessages) {
285
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
286
+ const liveMessages = this.chatDisplay.getDomLive(agentId);
287
+ const messages = this.chatDisplay.getDisplayMessages(agentId);
288
+ assertMonotonicLenta(messages, agentId.slice(0, 8));
289
+ const source = liveMessages.length ? 'hybrid' : 'jsonl';
290
+ return {
291
+ historyMessages,
292
+ liveMessages,
293
+ messages,
294
+ totalMessages: totalMessages ?? messages.length,
295
+ source: source,
296
+ };
297
+ }
298
+ emitLentaForAgent(agentId, opts) {
299
+ const snap = this.lentaSnapshot(agentId, opts.totalMessages);
300
+ const historyOut = opts.historyPayload ?? snap.historyMessages;
301
+ if (!historyOut.length && !snap.liveMessages.length)
302
+ return;
303
+ this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, {
304
+ totalMessages: opts.totalMessages,
305
+ append: opts.append,
306
+ reason: opts.reason,
307
+ forceSeq: opts.forceSeq,
308
+ });
309
+ }
310
+ lentaTailSignature(messages) {
311
+ return messages
312
+ .slice(-4)
313
+ .map((m) => `${m.role}:${m.id ?? ''}:${(m.text ?? '').length}:${(m.text ?? '').slice(-48)}`)
314
+ .join('|');
315
+ }
316
+ /** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
167
317
  emitJsonlLiveForAgent(agentId, rows, totalMessages) {
168
- const prevDisplay = this.chatDisplay.getJsonlHistory(agentId);
169
- const prevLen = prevDisplay.length;
170
- const prevLast = prevLen ? prevDisplay[prevLen - 1] : undefined;
318
+ const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
319
+ const prevSig = this.lastEmittedLentaSig.get(agentId);
320
+ const state = this.stateManager.getState();
321
+ this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
171
322
  this.chatDisplay.setJsonlBaseline(agentId, rows);
172
323
  this.lastEmittedJsonlRows.set(agentId, totalMessages);
324
+ this.writeLentaCapture(agentId, 'jsonl_file');
173
325
  const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
174
- if (!historyMessages.length)
326
+ const liveN = this.chatDisplay.getDomLive(agentId).length;
327
+ if (!historyMessages.length && !liveN)
175
328
  return;
176
- if (!prevLen) {
177
- this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
329
+ if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
330
+ return;
331
+ const storeMessages = this.chatDisplay.getDisplayMessages(agentId);
332
+ const deliveryPending = isLentaDeliveryPending(agentId, storeMessages);
333
+ const sig = this.lentaTailSignature(historyMessages);
334
+ const totalGrew = totalMessages > prevTotal;
335
+ const generating = isAgentGenerating(agentId, state);
336
+ if (!deliveryPending && !generating && !totalGrew && sig === prevSig && prevSig)
337
+ return;
338
+ const histLen = historyMessages.length;
339
+ const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
340
+ if (prevHistLen > 24 && histLen < prevHistLen - 12) {
341
+ bridgePipelineLog({
342
+ dir: 'internal',
343
+ event: 'agent:messages:SKIP_REGRESSION',
344
+ agentId,
345
+ msgs: histLen,
346
+ detail: `prev=${prevHistLen} total=${totalMessages}`,
347
+ });
178
348
  return;
179
349
  }
180
- const appended = historyMessages.slice(prevLen);
181
- if (appended.length) {
182
- this.emitAgentMessages(agentId, appended, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
350
+ this.emitLentaForAgent(agentId, {
351
+ totalMessages,
352
+ reason: 'jsonl_live',
353
+ forceSeq: deliveryPending || generating,
354
+ });
355
+ this.lastEmittedLentaSig.set(agentId, sig);
356
+ this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
357
+ if (isLentaDeliveryPending(agentId, this.chatDisplay.getDisplayMessages(agentId))) {
358
+ this.flushLentaDelivery(agentId, 'delivery_flush');
359
+ }
360
+ this.agentCompletionPush?.retryPendingContent(state);
361
+ }
362
+ /** Ingest CDP viewport into ChatDisplayStore — no debounce (debug + compose). */
363
+ ingestDomOverlayFromState(state) {
364
+ if (!state.messages?.length)
183
365
  return;
366
+ const domRows = this.prepareStateMessages(state.messages);
367
+ const targets = this.resolveDomOverlayAgentIds(state);
368
+ if (!targets.size)
369
+ return;
370
+ for (const agentId of targets) {
371
+ this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
372
+ if (!this.chatDisplay.getJsonlHistory(agentId).length) {
373
+ void this.ensureJsonlBaselineForAgent(agentId, state);
374
+ }
375
+ const prevLive = this.chatDisplay.getDomLive(agentId).length;
376
+ this.chatDisplay.mergeLiveForAgent(agentId, domRows);
377
+ const liveN = this.chatDisplay.getDomLive(agentId).length;
378
+ if (liveN > 0 && liveN !== prevLive) {
379
+ bridgePipelineLog({
380
+ dir: 'internal',
381
+ event: 'dom:overlay:ingest',
382
+ agentId,
383
+ msgs: liveN,
384
+ detail: `domRows=${domRows.length} generating=${isAgentGenerating(agentId, state)} viewport=${domRows.length}`,
385
+ });
386
+ }
387
+ this.writeLentaCapture(agentId, 'dom_ingest');
184
388
  }
185
- const last = historyMessages[historyMessages.length - 1];
186
- if (prevLast &&
187
- last &&
188
- last.role === prevLast.role &&
189
- (last.text ?? '') !== (prevLast.text ?? '')) {
190
- this.emitAgentMessages(agentId, [last], [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
389
+ }
390
+ /** Push lenta with DOM live to clients while generation is in flight. */
391
+ emitDomLiveWhileGenerating(state) {
392
+ for (const agentId of this.resolveDomOverlayAgentIds(state)) {
393
+ if (!isAgentGenerating(agentId, state))
394
+ continue;
395
+ if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
396
+ continue;
397
+ const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
398
+ if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
399
+ continue;
400
+ const snap = this.lentaSnapshot(agentId, this.lastEmittedJsonlRows.get(agentId) ?? undefined);
401
+ if (!snap.liveMessages.length)
402
+ continue;
403
+ this.lastEmittedLentaSig.delete(agentId);
404
+ this.emitLentaForAgent(agentId, {
405
+ totalMessages: snap.totalMessages,
406
+ reason: 'dom_overlay',
407
+ forceSeq: true,
408
+ });
409
+ }
410
+ }
411
+ syncGeneratingFlags(state) {
412
+ const targets = this.resolveDomOverlayAgentIds(state);
413
+ for (const agentId of targets) {
414
+ this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
415
+ }
416
+ }
417
+ /** Push composed lenta to subscribed clients (debounced). */
418
+ emitDomOverlaySocketForSubscribers(state) {
419
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
420
+ if (!isChatSyncedWithCursor(subId, meta.title, state))
421
+ continue;
422
+ const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
423
+ const deliveryPending = isLentaDeliveryPending(subId, snap.messages);
424
+ if (!snap.liveMessages.length && !state.agentWorking && !deliveryPending)
425
+ continue;
426
+ this.lastEmittedLentaSig.delete(subId);
427
+ this.emitLentaForAgent(subId, {
428
+ totalMessages: snap.totalMessages,
429
+ reason: 'dom_overlay',
430
+ forceSeq: deliveryPending,
431
+ });
432
+ }
433
+ }
434
+ /** While generating, push overlay growth without waiting for emit debounce tail. */
435
+ maybeEmitDomOverlayLeading(state) {
436
+ if (!state.agentWorking)
191
437
  return;
438
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
439
+ if (!isChatSyncedWithCursor(subId, meta.title, state))
440
+ continue;
441
+ if (!this.chatDisplay.getDomLive(subId).length)
442
+ continue;
443
+ const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
444
+ const sig = messagesFingerprint(snap.messages);
445
+ const last = getLastLentaEmit(subId);
446
+ if (last?.messagesSig === sig)
447
+ continue;
448
+ this.emitLentaForAgent(subId, {
449
+ totalMessages: snap.totalMessages,
450
+ reason: 'dom_overlay',
451
+ });
192
452
  }
193
- if (prevLen !== historyMessages.length || !this.jsonlIndex.getSubscribedAgents().has(agentId)) {
194
- this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
453
+ }
454
+ applyDomOverlayFromState(state) {
455
+ this.syncGeneratingFlags(state);
456
+ this.ingestDomOverlayFromState(state);
457
+ this.emitDomLiveWhileGenerating(state);
458
+ this.maybeEmitDomOverlayLeading(state);
459
+ this.scheduleDomOverlayEmit();
460
+ }
461
+ scheduleDomOverlayEmit() {
462
+ if (this.domOverlayTimer)
463
+ clearTimeout(this.domOverlayTimer);
464
+ this.domOverlayTimer = setTimeout(() => {
465
+ this.domOverlayTimer = null;
466
+ this.emitDomOverlaySocketForSubscribers(this.stateManager.getState());
467
+ }, Relay.DOM_OVERLAY_EMIT_MS);
468
+ }
469
+ /**
470
+ * Agent ids that should receive DOM ingest: active Cursor composer + synced subscriptions.
471
+ * Store must update even without phone subscribe (debug/lenta, later subscribe).
472
+ */
473
+ resolveDomOverlayAgentIds(state) {
474
+ const targets = new Set();
475
+ const activeTab = state.tabs.find((t) => t.active);
476
+ const activeId = resolveCursorActiveComposerId(state);
477
+ if (isComposerUuid(activeId))
478
+ targets.add(activeId);
479
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
480
+ if (!isChatSyncedWithCursor(subId, meta.title, state))
481
+ continue;
482
+ targets.add(subId);
483
+ if (isComposerUuid(subId))
484
+ continue;
485
+ const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
486
+ title: meta.title,
487
+ composerIdByTitle: state.composerIdByTitle,
488
+ activeComposerId: state.activeComposerId,
489
+ activeTabTitle: activeTab?.title,
490
+ });
491
+ if (path)
492
+ targets.add(basename(path, '.jsonl'));
195
493
  }
494
+ return targets;
196
495
  }
197
- /** Client lenta: JSONL only (`liveMessages` always empty). DOM stays in debug snapshot. */
198
- agentMessagesSnapshot(agentId, totalMessages) {
199
- const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
496
+ writeLentaCapture(agentId, reason, extra) {
497
+ const state = this.stateManager.getState();
498
+ const lenta = this.chatDisplay.getLentaDebug(agentId);
499
+ const activeTab = state.tabs.find((t) => t.active);
500
+ const jsonlPath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
501
+ title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
502
+ composerIdByTitle: state.composerIdByTitle,
503
+ activeComposerId: state.activeComposerId,
504
+ activeTabTitle: activeTab?.title,
505
+ });
506
+ const domViewport = resolveCursorActiveComposerId(state) === agentId
507
+ ? this.prepareStateMessages(state.messages)
508
+ : [];
509
+ captureLentaIfEnabled(agentId, {
510
+ reason,
511
+ source: lenta.source,
512
+ emitSeq: extra?.emitSeq,
513
+ seqHeld: extra?.seqHeld,
514
+ transition: extra?.transition,
515
+ jsonlRowCount: lenta.jsonlRowCount,
516
+ historyMessages: lenta.jsonlHistory,
517
+ liveMessages: lenta.domOverlay,
518
+ messages: lenta.messages,
519
+ domRaw: lenta.domRaw,
520
+ domViewport,
521
+ jsonlFilePath: jsonlPath ?? undefined,
522
+ activeComposerId: state.activeComposerId,
523
+ agentWorking: state.agentWorking,
524
+ });
525
+ }
526
+ resolveHistoryOpts(agentId) {
527
+ const state = this.stateManager.getState();
528
+ const activeTab = state.tabs.find((t) => t.active);
200
529
  return {
201
- historyMessages,
202
- liveMessages: [],
203
- messages: historyMessages,
204
- totalMessages: totalMessages ?? historyMessages.length,
530
+ title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
531
+ composerIdByTitle: state.composerIdByTitle,
532
+ activeComposerId: state.activeComposerId,
533
+ activeTabTitle: activeTab?.title,
205
534
  };
206
535
  }
536
+ agentMessagesSnapshot(agentId, totalMessages) {
537
+ return this.lentaSnapshot(agentId, totalMessages);
538
+ }
207
539
  checkMediaAuth(req) {
208
540
  if (!this.authEnabled)
209
541
  return true;
@@ -218,8 +550,11 @@ export class Relay {
218
550
  res.json({
219
551
  ok: true,
220
552
  cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
553
+ keepAwake: isKeepAwakeActive(),
221
554
  debugChats: '/debug/chats',
222
555
  debugSnapshot: '/debug/chat-snapshot',
556
+ debugLenta: '/debug/lenta?agentId=',
557
+ debugLentaSeq: '/debug/lenta-seq?agentId=',
223
558
  debugJsonlLive: '/debug/jsonl-live',
224
559
  });
225
560
  });
@@ -239,6 +574,67 @@ export class Relay {
239
574
  this.app.get('/debug/chat-snapshot', (_req, res) => {
240
575
  res.json(this.readOnlyChatSnapshot());
241
576
  });
577
+ this.app.get('/debug/lenta', async (req, res) => {
578
+ const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : '';
579
+ if (!agentId) {
580
+ res.status(400).json({ error: 'agentId required' });
581
+ return;
582
+ }
583
+ const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
584
+ const reloadJsonl = req.query.reloadJsonl === '1' || req.query.reloadJsonl === 'true';
585
+ try {
586
+ const report = await this.buildLentaDebugReport(agentId, title, { reloadJsonl });
587
+ res.json(report);
588
+ }
589
+ catch (err) {
590
+ res.status(500).json({ error: err.message });
591
+ }
592
+ });
593
+ this.app.post('/debug/lenta-capture/start', express.json(), (req, res) => {
594
+ const body = (req.body ?? {});
595
+ const agentId = String(body.agentId ?? (typeof req.query.agentId === 'string' ? req.query.agentId : '') ?? '').trim();
596
+ if (!agentId) {
597
+ res.status(400).json({ error: 'agentId required' });
598
+ return;
599
+ }
600
+ writeCaptureConfig({
601
+ enabled: true,
602
+ agentId,
603
+ title: body?.title?.trim(),
604
+ });
605
+ mkdirSync(captureSessionDir(agentId), { recursive: true });
606
+ res.json({
607
+ ok: true,
608
+ agentId,
609
+ sessionDir: captureSessionDir(agentId),
610
+ hint: 'Reproduce chat activity; snapshots append under sessionDir',
611
+ });
612
+ });
613
+ this.app.post('/debug/lenta-capture/stop', (_req, res) => {
614
+ const cfg = readCaptureConfig();
615
+ clearCaptureConfig();
616
+ res.json({ ok: true, stopped: cfg?.agentId ?? null });
617
+ });
618
+ this.app.get('/debug/lenta-capture/status', (_req, res) => {
619
+ const cfg = readCaptureConfig();
620
+ res.json({
621
+ config: cfg,
622
+ sessionDir: cfg ? captureSessionDir(cfg.agentId) : null,
623
+ steps: cfg ? listCaptureSteps(cfg.agentId) : [],
624
+ });
625
+ });
626
+ this.app.get('/debug/lenta-seq', (req, res) => {
627
+ const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : undefined;
628
+ const journal = getLentaSeqJournal(agentId || undefined);
629
+ res.json({
630
+ at: new Date().toISOString(),
631
+ agentId: agentId ?? null,
632
+ logFile: getLentaSeqLogPath(),
633
+ currentSeq: agentId ? this.historySeqByAgent.get(agentId) ?? 0 : undefined,
634
+ journal,
635
+ handoffs: journal.filter((e) => e.transition === 'dom_to_jsonl'),
636
+ });
637
+ });
242
638
  this.app.get('/debug/jsonl-live', async (req, res) => {
243
639
  if (!this.checkMediaAuth(req)) {
244
640
  res.status(401).json({ error: 'Unauthorized' });
@@ -342,29 +738,24 @@ export class Relay {
342
738
  detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
343
739
  });
344
740
  try {
741
+ const full = await this.jsonlIndex.loadHistory(agentId, {
742
+ title,
743
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
744
+ offset,
745
+ });
746
+ this.chatDisplay.setJsonlBaseline(agentId, full.messages);
747
+ const history = limit > 0 && full.messages.length > limit
748
+ ? { ...full, messages: full.messages.slice(-limit) }
749
+ : full;
345
750
  if (!isChatHistoryFromJsonl()) {
346
- const history = await this.jsonlIndex.loadHistory(agentId, {
347
- title,
348
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
349
- limit,
350
- offset,
351
- });
352
- this.chatDisplay.setJsonlBaseline(agentId, history.messages);
353
751
  res.json({
354
752
  agentId,
355
- ...this.agentMessagesSnapshot(agentId, history.totalMessages),
753
+ ...this.agentMessagesSnapshot(agentId, full.totalMessages),
356
754
  requestId,
357
755
  updatedAt: Date.now(),
358
756
  });
359
757
  return;
360
758
  }
361
- const history = await this.jsonlIndex.loadHistory(agentId, {
362
- title,
363
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
364
- limit,
365
- offset,
366
- });
367
- this.chatDisplay.setJsonlBaseline(agentId, history.messages);
368
759
  const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
369
760
  const body = { ...history, ...snap, requestId, updatedAt: Date.now() };
370
761
  const bytes = JSON.stringify(body).length;
@@ -528,6 +919,16 @@ export class Relay {
528
919
  this.io.on('connection', (socket) => this.onConnect(socket));
529
920
  }
530
921
  broadcast(event, payload) {
922
+ if (event === 'agent:messages') {
923
+ const p = payload;
924
+ bridgePipelineLog({
925
+ dir: 'out',
926
+ event: 'broadcast:agent:messages',
927
+ agentId: p?.agentId,
928
+ msgs: p?.historyMessages?.length ?? 0,
929
+ detail: `live=${p?.liveMessages?.length ?? 0} append=${!!p?.append} seq=${p?.seq ?? '-'} upstream=${this.upstream?.connected ?? false}`,
930
+ });
931
+ }
531
932
  if (event === 'agents:history' || event === 'agent:history') {
532
933
  const h = payload;
533
934
  const bytes = JSON.stringify(payload ?? {}).length;
@@ -550,22 +951,60 @@ export class Relay {
550
951
  this.historySeqByAgent.set(agentId, n);
551
952
  return n;
552
953
  }
553
- emitAgentMessages(agentId, historyMessages, liveMessages, source, totalMessages, seq, append = false) {
554
- const outSeq = seq ?? this.nextHistorySeq(agentId);
954
+ emitAgentMessages(agentId, historyMessages, liveMessages, source, opts) {
555
955
  const messages = [...historyMessages, ...liveMessages];
956
+ const sig = messagesFingerprint(messages);
957
+ const prevSeq = this.historySeqByAgent.get(agentId) ?? 0;
958
+ const prevHeld = getLastLentaEmit(agentId);
959
+ let outSeq;
960
+ let seqHeld = false;
961
+ if (opts?.forceSeq) {
962
+ outSeq = this.nextHistorySeq(agentId);
963
+ }
964
+ else if (prevHeld && prevHeld.messagesSig === sig) {
965
+ outSeq = prevHeld.seq;
966
+ seqHeld = true;
967
+ this.historySeqByAgent.set(agentId, outSeq);
968
+ }
969
+ else {
970
+ outSeq = this.nextHistorySeq(agentId);
971
+ }
972
+ const entry = recordLentaSeqEmit(agentId, outSeq, prevSeq, seqHeld, messages, {
973
+ reason: opts?.reason ?? 'emit',
974
+ source,
975
+ historyLen: historyMessages.length,
976
+ liveLen: liveMessages.length,
977
+ messagesLen: messages.length,
978
+ append: opts?.append,
979
+ });
980
+ bridgePipelineLog({
981
+ dir: 'out',
982
+ event: 'lenta:seq',
983
+ agentId,
984
+ msgs: messages.length,
985
+ detail: `seq=${outSeq} prev=${prevSeq} held=${seqHeld ? 1 : 0} ` +
986
+ `reason=${entry.reason} transition=${entry.transition} ` +
987
+ `hist=${entry.historyLen} live=${entry.liveLen} src=${source}`,
988
+ });
556
989
  this.broadcast('agent:messages', {
557
990
  agentId,
558
991
  historyMessages,
559
992
  liveMessages,
560
993
  messages,
561
- totalMessages: totalMessages ?? messages.length,
994
+ totalMessages: opts?.totalMessages ?? messages.length,
562
995
  source,
563
996
  updatedAt: Date.now(),
564
997
  seq: outSeq,
565
- append: append || undefined,
998
+ append: opts?.append || undefined,
999
+ });
1000
+ this.writeLentaCapture(agentId, 'emit_agent_messages', {
1001
+ emitSeq: outSeq,
1002
+ seqHeld,
1003
+ transition: entry.transition,
566
1004
  });
1005
+ this.noteLentaDelivery(agentId, messages);
1006
+ this.refreshLentaPendingPatch();
567
1007
  }
568
- /** DOM messages in state:patch — debug/UI chrome only; chat lenta is JSONL via agent:messages. */
569
1008
  prepareStateMessages(raw) {
570
1009
  return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
571
1010
  }
@@ -577,15 +1016,125 @@ export class Relay {
577
1016
  messages: this.prepareStateMessages(payload.messages),
578
1017
  };
579
1018
  }
1019
+ buildLentaPendingByAgent() {
1020
+ const state = this.stateManager.getState();
1021
+ const out = {};
1022
+ for (const [agentId, meta] of this.jsonlIndex.getSubscribedAgents()) {
1023
+ if (!isChatSyncedWithCursor(agentId, meta.title, state))
1024
+ continue;
1025
+ const messages = this.chatDisplay.getDisplayMessages(agentId);
1026
+ if (isLentaDeliveryPending(agentId, messages))
1027
+ out[agentId] = true;
1028
+ }
1029
+ return out;
1030
+ }
1031
+ refreshLentaPendingCache() {
1032
+ this.cachedLentaPendingByAgent = this.buildLentaPendingByAgent();
1033
+ }
1034
+ withRelayState(payload) {
1035
+ const base = this.withDisplayState(payload);
1036
+ if (payload.messages?.length) {
1037
+ this.refreshLentaPendingCache();
1038
+ }
1039
+ return {
1040
+ ...base,
1041
+ lentaPendingByAgent: this.cachedLentaPendingByAgent,
1042
+ updatedAt: base.updatedAt ?? Date.now(),
1043
+ };
1044
+ }
1045
+ refreshLentaPendingPatch() {
1046
+ this.refreshLentaPendingCache();
1047
+ this.broadcast('state:patch', {
1048
+ lentaPendingByAgent: this.cachedLentaPendingByAgent,
1049
+ updatedAt: Date.now(),
1050
+ });
1051
+ }
1052
+ noteLentaDelivery(agentId, storeMessages) {
1053
+ if (isLentaDeliveryPending(agentId, storeMessages)) {
1054
+ if (!this.lentaPendingSince.has(agentId)) {
1055
+ this.lentaPendingSince.set(agentId, Date.now());
1056
+ }
1057
+ return;
1058
+ }
1059
+ this.lentaPendingSince.delete(agentId);
1060
+ this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
1061
+ }
1062
+ /** Push `agent:messages` when store is ahead of last emit (subscribed + synced). */
1063
+ flushLentaDelivery(agentId, reason) {
1064
+ if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
1065
+ return false;
1066
+ const state = this.stateManager.getState();
1067
+ const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
1068
+ if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
1069
+ return false;
1070
+ const messages = this.chatDisplay.getDisplayMessages(agentId);
1071
+ if (!isLentaDeliveryPending(agentId, messages)) {
1072
+ this.lentaPendingSince.delete(agentId);
1073
+ return false;
1074
+ }
1075
+ const since = this.lentaPendingSince.get(agentId) ?? Date.now();
1076
+ if (!this.lentaPendingSince.has(agentId))
1077
+ this.lentaPendingSince.set(agentId, since);
1078
+ bridgePipelineLog({
1079
+ dir: 'internal',
1080
+ event: 'lenta:delivery:FLUSH',
1081
+ agentId,
1082
+ msgs: messages.length,
1083
+ detail: `reason=${reason} ageMs=${Date.now() - since}`,
1084
+ });
1085
+ this.emitLentaForAgent(agentId, {
1086
+ totalMessages: this.lastEmittedJsonlRows.get(agentId) ?? messages.length,
1087
+ reason,
1088
+ forceSeq: true,
1089
+ });
1090
+ this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
1091
+ return true;
1092
+ }
1093
+ flushPendingLentaForSubscribers(reason) {
1094
+ for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
1095
+ this.flushLentaDelivery(agentId, reason);
1096
+ }
1097
+ this.refreshLentaPendingPatch();
1098
+ }
1099
+ maybeForceStaleLentaDelivery() {
1100
+ const now = Date.now();
1101
+ for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
1102
+ const since = this.lentaPendingSince.get(agentId);
1103
+ if (since == null)
1104
+ continue;
1105
+ if (now - since < Relay.LENTA_PENDING_FORCE_MS)
1106
+ continue;
1107
+ this.flushLentaDelivery(agentId, 'delivery_flush');
1108
+ }
1109
+ }
580
1110
  wireEvents() {
581
1111
  this.stateManager.on('state:full', (state) => {
582
- this.broadcast('state:full', this.withDisplayState(state));
1112
+ this.refreshLentaPendingCache();
1113
+ this.broadcast('state:full', this.withRelayState(state));
583
1114
  this.emitAgentsIndex();
584
1115
  this.observeAgentCompletionForPush();
1116
+ if (state.messages?.length)
1117
+ this.applyDomOverlayFromState(state);
1118
+ if (!state.agentWorking)
1119
+ this.flushPendingLentaForSubscribers('dom_overlay');
1120
+ this.maybeForceStaleLentaDelivery();
585
1121
  });
586
1122
  this.stateManager.on('state:patch', (patch) => {
587
- this.broadcast('state:patch', this.withDisplayState(patch));
1123
+ this.broadcast('state:patch', this.withRelayState(patch));
588
1124
  this.observeAgentCompletionForPush();
1125
+ const state = this.stateManager.getState();
1126
+ if (patch.messages?.length || state.messages?.length) {
1127
+ this.applyDomOverlayFromState(state);
1128
+ }
1129
+ if (patch.agentWorking === false || (!state.agentWorking && patch.agentWorking !== true)) {
1130
+ this.syncGeneratingFlags(state);
1131
+ this.flushPendingLentaForSubscribers('jsonl_live');
1132
+ this.refreshLentaPendingCache();
1133
+ }
1134
+ else if (state.agentWorking) {
1135
+ this.syncGeneratingFlags(state);
1136
+ }
1137
+ this.maybeForceStaleLentaDelivery();
589
1138
  if (patch.sidebarRepos || patch.composerIdByTitle) {
590
1139
  const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
591
1140
  if (key !== this.lastSidebarIndexKey) {
@@ -605,7 +1154,7 @@ export class Relay {
605
1154
  this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
606
1155
  const total = payload.totalMessages ?? payload.messages.length;
607
1156
  this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
608
- this.agentCompletionPush?.onJsonlUpdated(payload.agentId, payload.messages, this.stateManager.getState());
1157
+ this.writeLentaCapture(payload.agentId, 'jsonl_file');
609
1158
  });
610
1159
  this.jsonlIndex.on('agent:history', (history) => {
611
1160
  const total = history.totalMessages ?? history.messages.length;
@@ -655,7 +1204,7 @@ export class Relay {
655
1204
  });
656
1205
  }
657
1206
  pushFullStateToRemote() {
658
- this.broadcast('state:full', this.withDisplayState(this.stateManager.getState()));
1207
+ this.broadcast('state:full', this.withRelayState(this.stateManager.getState()));
659
1208
  if (this.lastJsonlIndex.updatedAt > 0) {
660
1209
  this.emitAgentsIndex(true);
661
1210
  return;
@@ -692,7 +1241,7 @@ export class Relay {
692
1241
  this.dispatchClientEvent(event, args, reply);
693
1242
  }
694
1243
  onConnect(socket) {
695
- socket.emit('state:full', this.withDisplayState(this.stateManager.getState()));
1244
+ socket.emit('state:full', this.withRelayState(this.stateManager.getState()));
696
1245
  const sendIndex = (index) => {
697
1246
  socket.emit('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
698
1247
  };
@@ -795,36 +1344,28 @@ export class Relay {
795
1344
  detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
796
1345
  });
797
1346
  try {
1347
+ const full = await this.jsonlIndex.loadHistory(agentId, {
1348
+ title,
1349
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
1350
+ offset,
1351
+ });
1352
+ this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
1353
+ const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
798
1354
  if (!isChatHistoryFromJsonl()) {
799
- const history = await this.jsonlIndex.loadHistory(agentId, {
800
- title,
801
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
802
- limit: socketLimit,
803
- offset,
804
- });
805
- this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
806
- const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
807
- const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
1355
+ const seq = this.historySeqByAgent.get(agentId) ?? 0;
808
1356
  const ms = Date.now() - t0;
809
- console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${history.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
1357
+ console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
810
1358
  reply('agents:history', {
811
1359
  agentId,
812
1360
  ...snap,
813
- totalMessages: history.totalMessages,
1361
+ totalMessages: full.totalMessages,
814
1362
  requestId,
815
1363
  updatedAt: Date.now(),
816
1364
  seq,
817
1365
  });
818
1366
  return;
819
1367
  }
820
- const history = await this.jsonlIndex.loadHistory(agentId, {
821
- title,
822
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
823
- limit: socketLimit,
824
- offset,
825
- });
826
- this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
827
- const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
1368
+ const history = full;
828
1369
  const ms = Date.now() - t0;
829
1370
  const bytes = JSON.stringify(snap.messages).length;
830
1371
  console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
@@ -837,7 +1378,7 @@ export class Relay {
837
1378
  msgs: history.messages?.length ?? 0,
838
1379
  detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
839
1380
  });
840
- const seq = this.nextHistorySeq(history.agentId);
1381
+ const seq = this.historySeqByAgent.get(history.agentId) ?? 0;
841
1382
  reply('agents:history', {
842
1383
  ...history,
843
1384
  historyMessages: snap.historyMessages,
@@ -872,11 +1413,12 @@ export class Relay {
872
1413
  async runAgentsSubscribe({ agentId, title, focus, }) {
873
1414
  if (!agentId)
874
1415
  return;
875
- this.jsonlIndex.subscribe(agentId, title, { emitHistory: true });
1416
+ const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
1417
+ this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
876
1418
  this.stateManager.patchNow({ lastError: undefined });
877
1419
  await this.trySwitchWindowForAgent(agentId);
878
1420
  if (focus === false) {
879
- void this.refreshDomChatOnSubscribe(agentId, title);
1421
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
880
1422
  return;
881
1423
  }
882
1424
  const result = await this.commandExecutor.execute({
@@ -888,18 +1430,43 @@ export class Relay {
888
1430
  if (!result.ok) {
889
1431
  console.warn('[relay] subscribe focus (non-fatal):', result.error);
890
1432
  }
891
- void this.refreshDomChatOnSubscribe(agentId, title);
1433
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
1434
+ }
1435
+ /** Load JSONL into store when DOM ingest runs but baseline empty (e.g. after bridge restart). */
1436
+ async ensureJsonlBaselineForAgent(agentId, state) {
1437
+ if (this.chatDisplay.getJsonlHistory(agentId).length > 0)
1438
+ return;
1439
+ const sub = this.jsonlIndex.getSubscribedAgents().get(agentId);
1440
+ try {
1441
+ const history = await this.jsonlIndex.loadHistory(agentId, {
1442
+ title: sub?.title,
1443
+ composerIdByTitle: state.composerIdByTitle,
1444
+ });
1445
+ if (!history.messages.length)
1446
+ return;
1447
+ this.chatDisplay.setJsonlBaseline(agentId, history.messages);
1448
+ this.lastEmittedJsonlRows.set(agentId, history.totalMessages);
1449
+ this.domExtractor.pollNow();
1450
+ }
1451
+ catch (err) {
1452
+ console.warn('[relay] ensureJsonlBaseline (non-fatal):', err.message);
1453
+ }
892
1454
  }
893
1455
  /** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
894
- async refreshDomChatOnSubscribe(agentId, title) {
1456
+ async refreshDomChatOnSubscribe(agentId, title, opts) {
895
1457
  try {
896
- this.chatDisplay.clearAgent(agentId);
897
- this.lastEmittedJsonlRows.delete(agentId);
1458
+ if (opts?.clear !== false) {
1459
+ this.chatDisplay.clearAgent(agentId);
1460
+ this.lastEmittedJsonlRows.delete(agentId);
1461
+ this.lastEmittedLentaSig.delete(agentId);
1462
+ this.lastEmittedHistLen.delete(agentId);
1463
+ this.lentaPendingSince.delete(agentId);
1464
+ }
898
1465
  const history = await this.jsonlIndex.loadHistory(agentId, {
899
1466
  title,
900
1467
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
901
- limit: AGENT_HISTORY_DEFAULT_LIMIT,
902
1468
  });
1469
+ this.lastEmittedLentaSig.delete(agentId);
903
1470
  this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
904
1471
  await this.commandExecutor.scrollChatToBottom();
905
1472
  this.domExtractor.pollNow();
@@ -913,6 +1480,10 @@ export class Relay {
913
1480
  this.jsonlIndex.unsubscribe(agentId);
914
1481
  this.chatDisplay.clearAgent(agentId);
915
1482
  this.lastEmittedJsonlRows.delete(agentId);
1483
+ this.lastEmittedLentaSig.delete(agentId);
1484
+ this.lastEmittedHistLen.delete(agentId);
1485
+ this.lentaPendingSince.delete(agentId);
1486
+ this.refreshLentaPendingPatch();
916
1487
  }
917
1488
  }
918
1489
  async runAgentsFocus({ agentId, title }, reply) {