cursorconnect 0.1.8 → 0.1.10

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 (37) hide show
  1. package/bridge-runtime/.env.example +2 -0
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +18 -6
  4. package/bridge-runtime/dist/agent-completion-push.js +186 -41
  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 +96 -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/debug-chats-page.d.ts +1 -1
  14. package/bridge-runtime/dist/debug-chats-page.js +148 -26
  15. package/bridge-runtime/dist/dom-transcript-store.d.ts +2 -0
  16. package/bridge-runtime/dist/dom-transcript-store.js +17 -2
  17. package/bridge-runtime/dist/extract-page.js +5 -4
  18. package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
  19. package/bridge-runtime/dist/lenta-capture.js +146 -0
  20. package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
  21. package/bridge-runtime/dist/lenta-debug.js +221 -0
  22. package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
  23. package/bridge-runtime/dist/lenta-delivery.js +10 -0
  24. package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
  25. package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
  26. package/bridge-runtime/dist/message-filter.d.ts +5 -0
  27. package/bridge-runtime/dist/message-filter.js +4 -0
  28. package/bridge-runtime/dist/relay.d.ts +37 -3
  29. package/bridge-runtime/dist/relay.js +557 -51
  30. package/bridge-runtime/dist/types.d.ts +9 -4
  31. package/dist/bridge-build.js +50 -0
  32. package/dist/index.js +9 -6
  33. package/dist/launch.js +5 -1
  34. package/dist/run-service.js +10 -4
  35. package/dist/startup-check.js +6 -0
  36. package/package.json +1 -1
  37. package/version-policy.json +1 -1
@@ -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';
@@ -7,7 +8,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
10
  import { resolveHistoryLimit } from './history-request.js';
10
- import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
11
+ import { assertMonotonicLenta, filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
11
12
  import { isKeepAwakeActive } from './keep-awake.js';
12
13
  function sleepMs(ms) {
13
14
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -20,8 +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';
23
- import { isChatSyncedWithCursor } from './chat-sync.js';
24
+ import { isAgentGenerating, isChatSyncedWithCursor, isComposerUuid, resolveCursorActiveComposerId, } from './chat-sync.js';
24
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';
25
30
  export class Relay {
26
31
  stateManager;
27
32
  commandExecutor;
@@ -49,7 +54,12 @@ export class Relay {
49
54
  lastEmittedLentaSig = new Map();
50
55
  /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
51
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);
52
61
  domOverlayTimer = null;
62
+ cachedLentaPendingByAgent = {};
53
63
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
54
64
  this.stateManager = stateManager;
55
65
  this.commandExecutor = commandExecutor;
@@ -73,7 +83,15 @@ export class Relay {
73
83
  this.upstream = new RelayUpstream(this.config, (event, ...args) => {
74
84
  this.handleRemoteClientEvent(event, ...args);
75
85
  }, () => this.flushPendingPushPayloads());
76
- this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex);
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
+ });
77
95
  this.upstream.connect();
78
96
  }
79
97
  }
@@ -81,6 +99,7 @@ export class Relay {
81
99
  emitAgentCompletedPush(payload) {
82
100
  if (this.upstream?.connected) {
83
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)}`);
84
103
  return;
85
104
  }
86
105
  this.pendingPushPayloads.push(payload);
@@ -101,6 +120,10 @@ export class Relay {
101
120
  observeAgentCompletionForPush() {
102
121
  this.agentCompletionPush?.observe(this.stateManager.getState());
103
122
  }
123
+ requestAgentCompletionContentSync(agentId, state) {
124
+ void this.ensureJsonlBaselineForAgent(agentId, state);
125
+ this.flushLentaDelivery(agentId, 'push_ready');
126
+ }
104
127
  listen() {
105
128
  return new Promise((resolve) => {
106
129
  this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
@@ -112,23 +135,33 @@ export class Relay {
112
135
  get authEnabled() {
113
136
  return this.config.webappPassword.length > 0;
114
137
  }
138
+ lentaAgentIds(state) {
139
+ const subscribed = [...this.jsonlIndex.getSubscribedAgents().keys()];
140
+ return new Set([
141
+ ...subscribed,
142
+ ...(state.activeComposerId ? [state.activeComposerId] : []),
143
+ ]);
144
+ }
115
145
  /** Read-only view of in-memory bridge state (no writes to stores). */
116
146
  readOnlyChatSnapshot() {
117
147
  const state = this.stateManager.getState();
118
148
  const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
119
149
  const displayCache = {};
120
- const cacheIds = new Set([
121
- ...subscribed.map((s) => s.agentId),
122
- ...(state.activeComposerId ? [state.activeComposerId] : []),
123
- ]);
124
- const domTranscript = {};
125
- for (const agentId of cacheIds) {
126
- const dom = this.chatDisplay.getDomTranscript(agentId);
127
- if (dom.length)
128
- domTranscript[agentId] = dom;
129
- const jsonl = this.chatDisplay.getJsonlHistory(agentId);
130
- if (jsonl.length)
131
- 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;
132
165
  }
133
166
  return {
134
167
  at: Date.now(),
@@ -138,6 +171,7 @@ export class Relay {
138
171
  activeChatTitle: state.activeChatTitle,
139
172
  updatedAt: state.updatedAt,
140
173
  lastError: state.lastError,
174
+ agentWorking: state.agentWorking,
141
175
  tabs: state.tabs,
142
176
  composerIdByTitle: state.composerIdByTitle,
143
177
  domMessageCount: state.messages.length,
@@ -148,9 +182,83 @@ export class Relay {
148
182
  ringSize: this.messageDebugStore.list().length,
149
183
  },
150
184
  subscribed,
185
+ lentaByAgent,
151
186
  displayCache,
187
+ domOverlay,
188
+ displayMessages,
189
+ domRaw,
190
+ /** @deprecated use `domOverlay` */
191
+ domTranscript: domOverlay,
152
192
  chatHistoryJsonl: isChatHistoryFromJsonl(),
153
- 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,
154
262
  };
155
263
  }
156
264
  /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
@@ -172,16 +280,18 @@ export class Relay {
172
280
  this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
173
281
  }
174
282
  }
175
- /** JSONL baseline + DOM rows not yet in file (`liveMessages`). */
283
+ /** JSONL baseline + DOM tail (`liveMessages`); `messages` = append-only compose. */
176
284
  lentaSnapshot(agentId, totalMessages) {
177
285
  const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
178
286
  const liveMessages = this.chatDisplay.getDomLive(agentId);
287
+ const messages = this.chatDisplay.getDisplayMessages(agentId);
288
+ assertMonotonicLenta(messages, agentId.slice(0, 8));
179
289
  const source = liveMessages.length ? 'hybrid' : 'jsonl';
180
290
  return {
181
291
  historyMessages,
182
292
  liveMessages,
183
- messages: [...historyMessages, ...liveMessages],
184
- totalMessages: totalMessages ?? historyMessages.length,
293
+ messages,
294
+ totalMessages: totalMessages ?? messages.length,
185
295
  source: source,
186
296
  };
187
297
  }
@@ -190,7 +300,12 @@ export class Relay {
190
300
  const historyOut = opts.historyPayload ?? snap.historyMessages;
191
301
  if (!historyOut.length && !snap.liveMessages.length)
192
302
  return;
193
- this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, opts.totalMessages, opts.seq, opts.append);
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
+ });
194
309
  }
195
310
  lentaTailSignature(messages) {
196
311
  return messages
@@ -202,16 +317,23 @@ export class Relay {
202
317
  emitJsonlLiveForAgent(agentId, rows, totalMessages) {
203
318
  const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
204
319
  const prevSig = this.lastEmittedLentaSig.get(agentId);
320
+ const state = this.stateManager.getState();
321
+ this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
205
322
  this.chatDisplay.setJsonlBaseline(agentId, rows);
206
323
  this.lastEmittedJsonlRows.set(agentId, totalMessages);
324
+ this.writeLentaCapture(agentId, 'jsonl_file');
207
325
  const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
208
- if (!historyMessages.length && !this.chatDisplay.getDomLive(agentId).length)
326
+ const liveN = this.chatDisplay.getDomLive(agentId).length;
327
+ if (!historyMessages.length && !liveN)
209
328
  return;
210
329
  if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
211
330
  return;
331
+ const storeMessages = this.chatDisplay.getDisplayMessages(agentId);
332
+ const deliveryPending = isLentaDeliveryPending(agentId, storeMessages);
212
333
  const sig = this.lentaTailSignature(historyMessages);
213
334
  const totalGrew = totalMessages > prevTotal;
214
- if (!totalGrew && sig === prevSig && prevSig)
335
+ const generating = isAgentGenerating(agentId, state);
336
+ if (!deliveryPending && !generating && !totalGrew && sig === prevSig && prevSig)
215
337
  return;
216
338
  const histLen = historyMessages.length;
217
339
  const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
@@ -225,40 +347,191 @@ export class Relay {
225
347
  });
226
348
  return;
227
349
  }
228
- this.lastEmittedLentaSig.set(agentId, sig);
229
- this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
230
350
  this.emitLentaForAgent(agentId, {
231
351
  totalMessages,
232
- seq: this.nextHistorySeq(agentId),
352
+ reason: 'jsonl_live',
353
+ forceSeq: deliveryPending || generating,
233
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);
234
361
  }
235
- scheduleDomOverlayEmit() {
236
- if (this.domOverlayTimer)
237
- clearTimeout(this.domOverlayTimer);
238
- this.domOverlayTimer = setTimeout(() => {
239
- this.domOverlayTimer = null;
240
- this.emitDomOverlayForSyncedSubscribers();
241
- }, 120);
242
- }
243
- /** DOM poll while agent works — overlay until the same turn lands in JSONL. */
244
- emitDomOverlayForSyncedSubscribers() {
245
- const state = this.stateManager.getState();
362
+ /** Ingest CDP viewport into ChatDisplayStore — no debounce (debug + compose). */
363
+ ingestDomOverlayFromState(state) {
246
364
  if (!state.messages?.length)
247
365
  return;
248
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');
388
+ }
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) {
249
419
  for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
250
420
  if (!isChatSyncedWithCursor(subId, meta.title, state))
251
421
  continue;
252
- this.chatDisplay.mergeLiveForAgent(subId, domRows);
253
422
  const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
254
- if (!snap.liveMessages.length && !state.agentWorking)
423
+ const deliveryPending = isLentaDeliveryPending(subId, snap.messages);
424
+ if (!snap.liveMessages.length && !state.agentWorking && !deliveryPending)
255
425
  continue;
256
426
  this.lastEmittedLentaSig.delete(subId);
257
427
  this.emitLentaForAgent(subId, {
258
428
  totalMessages: snap.totalMessages,
259
- seq: this.nextHistorySeq(subId),
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)
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
+ });
452
+ }
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,
260
490
  });
491
+ if (path)
492
+ targets.add(basename(path, '.jsonl'));
261
493
  }
494
+ return targets;
495
+ }
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);
529
+ return {
530
+ title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
531
+ composerIdByTitle: state.composerIdByTitle,
532
+ activeComposerId: state.activeComposerId,
533
+ activeTabTitle: activeTab?.title,
534
+ };
262
535
  }
263
536
  agentMessagesSnapshot(agentId, totalMessages) {
264
537
  return this.lentaSnapshot(agentId, totalMessages);
@@ -280,6 +553,8 @@ export class Relay {
280
553
  keepAwake: isKeepAwakeActive(),
281
554
  debugChats: '/debug/chats',
282
555
  debugSnapshot: '/debug/chat-snapshot',
556
+ debugLenta: '/debug/lenta?agentId=',
557
+ debugLentaSeq: '/debug/lenta-seq?agentId=',
283
558
  debugJsonlLive: '/debug/jsonl-live',
284
559
  });
285
560
  });
@@ -299,6 +574,67 @@ export class Relay {
299
574
  this.app.get('/debug/chat-snapshot', (_req, res) => {
300
575
  res.json(this.readOnlyChatSnapshot());
301
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
+ });
302
638
  this.app.get('/debug/jsonl-live', async (req, res) => {
303
639
  if (!this.checkMediaAuth(req)) {
304
640
  res.status(401).json({ error: 'Unauthorized' });
@@ -615,20 +951,59 @@ export class Relay {
615
951
  this.historySeqByAgent.set(agentId, n);
616
952
  return n;
617
953
  }
618
- emitAgentMessages(agentId, historyMessages, liveMessages, source, totalMessages, seq, append = false) {
619
- const outSeq = seq ?? this.nextHistorySeq(agentId);
954
+ emitAgentMessages(agentId, historyMessages, liveMessages, source, opts) {
620
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
+ });
621
989
  this.broadcast('agent:messages', {
622
990
  agentId,
623
991
  historyMessages,
624
992
  liveMessages,
625
993
  messages,
626
- totalMessages: totalMessages ?? messages.length,
994
+ totalMessages: opts?.totalMessages ?? messages.length,
627
995
  source,
628
996
  updatedAt: Date.now(),
629
997
  seq: outSeq,
630
- 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,
631
1004
  });
1005
+ this.noteLentaDelivery(agentId, messages);
1006
+ this.refreshLentaPendingPatch();
632
1007
  }
633
1008
  prepareStateMessages(raw) {
634
1009
  return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
@@ -641,18 +1016,125 @@ export class Relay {
641
1016
  messages: this.prepareStateMessages(payload.messages),
642
1017
  };
643
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
+ }
644
1110
  wireEvents() {
645
1111
  this.stateManager.on('state:full', (state) => {
646
- this.broadcast('state:full', this.withDisplayState(state));
1112
+ this.refreshLentaPendingCache();
1113
+ this.broadcast('state:full', this.withRelayState(state));
647
1114
  this.emitAgentsIndex();
648
1115
  this.observeAgentCompletionForPush();
1116
+ if (state.messages?.length)
1117
+ this.applyDomOverlayFromState(state);
1118
+ if (!state.agentWorking)
1119
+ this.flushPendingLentaForSubscribers('dom_overlay');
1120
+ this.maybeForceStaleLentaDelivery();
649
1121
  });
650
1122
  this.stateManager.on('state:patch', (patch) => {
651
- this.broadcast('state:patch', this.withDisplayState(patch));
1123
+ this.broadcast('state:patch', this.withRelayState(patch));
652
1124
  this.observeAgentCompletionForPush();
653
- if (patch.messages?.length) {
654
- this.scheduleDomOverlayEmit();
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);
655
1136
  }
1137
+ this.maybeForceStaleLentaDelivery();
656
1138
  if (patch.sidebarRepos || patch.composerIdByTitle) {
657
1139
  const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
658
1140
  if (key !== this.lastSidebarIndexKey) {
@@ -672,6 +1154,7 @@ export class Relay {
672
1154
  this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
673
1155
  const total = payload.totalMessages ?? payload.messages.length;
674
1156
  this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
1157
+ this.writeLentaCapture(payload.agentId, 'jsonl_file');
675
1158
  });
676
1159
  this.jsonlIndex.on('agent:history', (history) => {
677
1160
  const total = history.totalMessages ?? history.messages.length;
@@ -721,7 +1204,7 @@ export class Relay {
721
1204
  });
722
1205
  }
723
1206
  pushFullStateToRemote() {
724
- this.broadcast('state:full', this.withDisplayState(this.stateManager.getState()));
1207
+ this.broadcast('state:full', this.withRelayState(this.stateManager.getState()));
725
1208
  if (this.lastJsonlIndex.updatedAt > 0) {
726
1209
  this.emitAgentsIndex(true);
727
1210
  return;
@@ -758,7 +1241,7 @@ export class Relay {
758
1241
  this.dispatchClientEvent(event, args, reply);
759
1242
  }
760
1243
  onConnect(socket) {
761
- socket.emit('state:full', this.withDisplayState(this.stateManager.getState()));
1244
+ socket.emit('state:full', this.withRelayState(this.stateManager.getState()));
762
1245
  const sendIndex = (index) => {
763
1246
  socket.emit('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
764
1247
  };
@@ -869,7 +1352,7 @@ export class Relay {
869
1352
  this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
870
1353
  const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
871
1354
  if (!isChatHistoryFromJsonl()) {
872
- const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
1355
+ const seq = this.historySeqByAgent.get(agentId) ?? 0;
873
1356
  const ms = Date.now() - t0;
874
1357
  console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
875
1358
  reply('agents:history', {
@@ -895,7 +1378,7 @@ export class Relay {
895
1378
  msgs: history.messages?.length ?? 0,
896
1379
  detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
897
1380
  });
898
- const seq = this.nextHistorySeq(history.agentId);
1381
+ const seq = this.historySeqByAgent.get(history.agentId) ?? 0;
899
1382
  reply('agents:history', {
900
1383
  ...history,
901
1384
  historyMessages: snap.historyMessages,
@@ -949,6 +1432,26 @@ export class Relay {
949
1432
  }
950
1433
  void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
951
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
+ }
1454
+ }
952
1455
  /** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
953
1456
  async refreshDomChatOnSubscribe(agentId, title, opts) {
954
1457
  try {
@@ -957,6 +1460,7 @@ export class Relay {
957
1460
  this.lastEmittedJsonlRows.delete(agentId);
958
1461
  this.lastEmittedLentaSig.delete(agentId);
959
1462
  this.lastEmittedHistLen.delete(agentId);
1463
+ this.lentaPendingSince.delete(agentId);
960
1464
  }
961
1465
  const history = await this.jsonlIndex.loadHistory(agentId, {
962
1466
  title,
@@ -978,6 +1482,8 @@ export class Relay {
978
1482
  this.lastEmittedJsonlRows.delete(agentId);
979
1483
  this.lastEmittedLentaSig.delete(agentId);
980
1484
  this.lastEmittedHistLen.delete(agentId);
1485
+ this.lentaPendingSince.delete(agentId);
1486
+ this.refreshLentaPendingPatch();
981
1487
  }
982
1488
  }
983
1489
  async runAgentsFocus({ agentId, title }, reply) {