clay-server 4.1.0-beta.6 → 4.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +661 -21
  2. package/LICENSES/MIT-legacy.txt +21 -0
  3. package/LICENSES/README.md +7 -0
  4. package/README.md +2 -2
  5. package/bin/cli.js +0 -2
  6. package/lib/daemon.js +0 -2
  7. package/lib/default-ai-runtime.js +117 -0
  8. package/lib/git-cli.js +1 -0
  9. package/lib/issue-work-session.js +10 -0
  10. package/lib/issues-comments.js +40 -0
  11. package/lib/issues-commit-verification.js +30 -0
  12. package/lib/issues-mcp-server.js +27 -0
  13. package/lib/issues-schema.js +119 -0
  14. package/lib/issues-service.js +339 -0
  15. package/lib/issues-store.js +210 -0
  16. package/lib/model-entry.js +15 -0
  17. package/lib/project-capsule-turn.js +1 -0
  18. package/lib/project-connection.js +2 -0
  19. package/lib/project-delegated-session.js +1 -0
  20. package/lib/project-filesystem.js +10 -10
  21. package/lib/project-issue-launch.js +154 -0
  22. package/lib/project-issues.js +58 -0
  23. package/lib/project-logs-mcp-server.js +8 -12
  24. package/lib/project-message-delivery.js +4 -2
  25. package/lib/project-models.js +71 -3
  26. package/lib/project-pending-message-queue.js +264 -0
  27. package/lib/project-scheduled-tasks.js +2 -4
  28. package/lib/project-sessions.js +82 -31
  29. package/lib/project-tool-llm-runtime.js +48 -0
  30. package/lib/project-user-message.js +178 -42
  31. package/lib/project-workspace-query.js +25 -0
  32. package/lib/project.js +102 -18
  33. package/lib/public/app.js +38 -4
  34. package/lib/public/css/default-ai.css +33 -0
  35. package/lib/public/css/input.css +89 -5
  36. package/lib/public/css/issues.css +101 -0
  37. package/lib/public/css/messages.css +19 -0
  38. package/lib/public/css/pending-message-queue.css +187 -0
  39. package/lib/public/css/scheduled-tasks.css +3 -4
  40. package/lib/public/index.html +25 -17
  41. package/lib/public/modules/app-connection.js +3 -0
  42. package/lib/public/modules/app-cursors.js +29 -12
  43. package/lib/public/modules/app-message-router.js +8 -0
  44. package/lib/public/modules/app-messages.js +12 -3
  45. package/lib/public/modules/clay-file-links.js +111 -0
  46. package/lib/public/modules/clay-issue-links.js +131 -0
  47. package/lib/public/modules/clay-log-links.js +2 -0
  48. package/lib/public/modules/cursor-sharing-preference.js +38 -0
  49. package/lib/public/modules/default-ai.js +377 -0
  50. package/lib/public/modules/filebrowser.js +43 -2
  51. package/lib/public/modules/input.js +7 -17
  52. package/lib/public/modules/issues-context.js +117 -0
  53. package/lib/public/modules/issues-render.js +29 -0
  54. package/lib/public/modules/issues.js +270 -0
  55. package/lib/public/modules/markdown.js +4 -0
  56. package/lib/public/modules/message-delivery.js +5 -3
  57. package/lib/public/modules/pending-message-image-editor.js +143 -0
  58. package/lib/public/modules/pending-message-queue-model.js +22 -0
  59. package/lib/public/modules/pending-message-queue.js +449 -0
  60. package/lib/public/modules/scheduled-task-engine.js +0 -50
  61. package/lib/public/modules/scheduled-tasks.js +7 -16
  62. package/lib/public/modules/sidebar-mobile.js +3 -0
  63. package/lib/public/modules/sticky-note-markdown.js +1 -0
  64. package/lib/public/modules/terminal.js +2 -0
  65. package/lib/public/modules/tool-palette-order.js +1 -0
  66. package/lib/public/style.css +2 -0
  67. package/lib/sdk-bridge.js +34 -11
  68. package/lib/server-cursor-sharing.js +65 -0
  69. package/lib/server-default-ai.js +265 -0
  70. package/lib/server-home-chat.js +29 -9
  71. package/lib/server-home-clay-entry.js +4 -1
  72. package/lib/server-home-debate-planning.js +32 -14
  73. package/lib/server-home-mate-creation.js +30 -11
  74. package/lib/server-home-models.js +14 -6
  75. package/lib/server-issue-links.js +18 -0
  76. package/lib/server.js +41 -0
  77. package/lib/session-notes-mcp-server.js +1 -1
  78. package/lib/session-search.js +4 -1
  79. package/lib/tool-llm.js +1 -0
  80. package/lib/users-default-ai-preferences.js +80 -0
  81. package/lib/users-preferences.js +35 -0
  82. package/lib/users.js +10 -0
  83. package/lib/workspace-query-projection.js +29 -0
  84. package/lib/workspace-query-service.js +19 -29
  85. package/lib/ws-schema.js +36 -1
  86. package/lib/yoke/adapters/claude-worker.js +4 -2
  87. package/lib/yoke/adapters/claude.js +5 -1
  88. package/lib/yoke/package.json +1 -1
  89. package/package.json +5 -6
  90. package/bin/claude-relay.js +0 -6
@@ -21,6 +21,7 @@ var fs = require("fs");
21
21
  * sendExtensionCommandAny, requestTabContext,
22
22
  * startFileWatch, stopFileWatch,
23
23
  * scheduleMessage, cancelScheduledMessage,
24
+ * pendingMessageQueue,
24
25
  * loadContextSources, saveContextSources,
25
26
  * digestDmTurn, gateMemory,
26
27
  * adapter - YOKE adapter instance
@@ -42,7 +43,7 @@ function attachUserMessage(ctx) {
42
43
  var sendTo = ctx.sendTo;
43
44
  var sendToSession = ctx.sendToSession;
44
45
  var sendToSessionOthers = ctx.sendToSessionOthers;
45
- var messageDelivery = createProjectMessageDelivery(sendTo);
46
+ var messageDelivery = createProjectMessageDelivery(sendTo, slug);
46
47
 
47
48
  var clients = ctx.clients;
48
49
  var opts = ctx.opts;
@@ -71,6 +72,7 @@ function attachUserMessage(ctx) {
71
72
  var scheduleMessage = ctx.scheduleMessage;
72
73
  var cancelScheduledMessage = ctx.cancelScheduledMessage;
73
74
  var sendScheduledMessageNow = ctx.sendScheduledMessageNow;
75
+ var pendingMessageQueue = ctx.pendingMessageQueue;
74
76
 
75
77
  var loadContextSources = ctx.loadContextSources;
76
78
  var saveContextSources = ctx.saveContextSources;
@@ -99,7 +101,27 @@ function attachUserMessage(ctx) {
99
101
 
100
102
  // --------------- Main handler ---------------
101
103
 
102
- function handleUserMessage(ws, msg) {
104
+ function handleUserMessage(ws, msg, forcedSession, internal) {
105
+ if (msg.type === "pending_message_get" || msg.type === "pending_message_edit" || msg.type === "pending_message_cancel" || msg.type === "pending_message_reorder" || msg.type === "pending_message_resume") {
106
+ var queueSession = forcedSession || getSessionForWs(ws);
107
+ if (!queueSession || !pendingMessageQueue) return true;
108
+ var queueActor = ws && ws._clayUser;
109
+ var queueResult;
110
+ if (msg.projectSlug !== slug) queueResult = { ok: false, error: "Project changed", sessionId: queueSession.localId, projectSlug: slug };
111
+ else if (msg.type === "pending_message_resume") {
112
+ queueResult = pendingMessageQueue.resume(queueSession, queueActor, msg.revision, msg.sessionId);
113
+ if (queueResult.ok && !queueResult.paused) {
114
+ queueSession._pendingMessageDrainPaused = false;
115
+ setImmediate(function () { consumePendingMessage(queueSession); });
116
+ }
117
+ }
118
+ else if (msg.type === "pending_message_get") queueResult = pendingMessageQueue.hydrate(queueSession, queueActor);
119
+ else if (msg.type === "pending_message_edit") queueResult = pendingMessageQueue.edit(queueSession, queueActor, msg.id, msg.revision, msg.sessionId, msg.message || {});
120
+ else if (msg.type === "pending_message_cancel") queueResult = pendingMessageQueue.cancel(queueSession, queueActor, msg.id, msg.revision, msg.sessionId);
121
+ else queueResult = pendingMessageQueue.reorder(queueSession, queueActor, msg.ids || [], msg.revision, msg.sessionId);
122
+ sendTo(ws, { type: "pending_message_result", requestId: msg.requestId || null, result: queueResult });
123
+ return true;
124
+ }
103
125
  // --- Sticky notes ---
104
126
  if (msg.type === "note_create") {
105
127
  var note = nm.create(msg);
@@ -303,17 +325,29 @@ function attachUserMessage(ctx) {
303
325
  if (msg.type !== "message") return false;
304
326
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return true;
305
327
 
306
- var session = getSessionForWs(ws);
328
+ var session = forcedSession || getSessionForWs(ws);
307
329
  if (!session) return true;
330
+ var pendingInternal = internal && internal.pendingConsumed ? internal : null;
331
+ function finishPendingEarly(outcome, reason) {
332
+ if (pendingInternal && typeof pendingInternal.complete === "function") pendingInternal.complete(outcome, reason);
333
+ }
308
334
 
309
335
  if (session.loopInterviewHandoff && session.loopInterviewHandoff.state === "starting") {
310
336
  sendTo(ws, { type: "error", text: "Wait for the approved Loop handoff to finish." });
337
+ finishPendingEarly("release", "Loop handoff is still starting");
311
338
  return true;
312
339
  }
313
340
 
341
+ var queuedReplay = pendingMessageQueue && pendingMessageQueue.inspect(session, ws && ws._clayUser, msg.clientMessageId);
342
+ if (!pendingInternal && queuedReplay) {
343
+ messageDelivery.acknowledge(ws, { clientMessageId: queuedReplay.clientMessageId }, null, session);
344
+ sendTo(ws, { type: "message_queued", clientMessageId: queuedReplay.clientMessageId, sessionId: session.localId, projectSlug: slug, state: queuedReplay.state, paused: pendingMessageQueue.isPaused(session), revision: pendingMessageQueue.getRevision() });
345
+ return true;
346
+ }
314
347
  var deliveryReceipt = messageDelivery.inspect(ws, session, msg);
315
348
  if (deliveryReceipt.duplicate) {
316
- messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(deliveryReceipt.recordedMessage));
349
+ messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(deliveryReceipt.recordedMessage), session);
350
+ finishPendingEarly(true);
317
351
  return true;
318
352
  }
319
353
 
@@ -330,23 +364,37 @@ function attachUserMessage(ctx) {
330
364
  type: "error",
331
365
  text: "This Split Worker is controlled by its Driver. Send the instruction to the Driver session instead.",
332
366
  });
367
+ finishPendingEarly(false, "Pending messages cannot be delivered to a Driver-operated Split Worker");
368
+ return true;
369
+ }
370
+ if (!pendingInternal && pendingMessageQueue && (pendingMessageQueue.hasActive(session) || session.isProcessing || session._queryStarting || session._awaitingTurnResult)) {
371
+ var queuedResult = pendingMessageQueue.admit(session, msg, ws && ws._clayUser);
372
+ if (queuedResult.ok) {
373
+ messageDelivery.acknowledge(ws, deliveryReceipt, null, session);
374
+ sendTo(ws, { type: "message_queued", clientMessageId: msg.clientMessageId || null, sessionId: session.localId, projectSlug: slug, state: "pending", paused: pendingMessageQueue.isPaused(session), revision: pendingMessageQueue.getRevision() });
375
+ } else {
376
+ sendTo(ws, { type: "error", text: queuedResult.error || "Unable to queue message" });
377
+ }
333
378
  return true;
334
379
  }
380
+ if (!pendingInternal) session._pendingMessageDrainPaused = false;
335
381
  if (msg.autonomousRunToken) {
336
382
  if (!ctx.autonomousRun || !ctx.autonomousRun.consume(session, msg)) {
337
383
  sendTo(ws, { type: "error", text: "This Until complete submission is stale or no longer armed." });
384
+ finishPendingEarly(false, "Until complete submission is stale or no longer armed");
338
385
  return true;
339
386
  }
340
387
  } else if (session.autonomousRun && session.autonomousRun.state === "armed") {
341
388
  sendTo(ws, { type: "error", text: "Until complete is still being armed. Wait for its acknowledgement before sending." });
389
+ finishPendingEarly("release", "Until complete is still being armed");
342
390
  return true;
343
391
  } else if (ctx.autonomousRun) {
344
392
  ctx.autonomousRun.onHumanMessage(session, msg);
345
393
  }
346
394
  var autonomousDeliveryMeta = session.autonomousRun && ["running", "reviewing", "waiting-worker", "waiting-user"].indexOf(session.autonomousRun.state) !== -1
347
395
  ? { autonomousRunId: session.autonomousRun.id } : undefined;
348
- if (typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
349
- if (gitAttribution) gitAttribution.beginTurn(session);
396
+ if (!pendingInternal && typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
397
+ if (!pendingInternal && gitAttribution) gitAttribution.beginTurn(session);
350
398
 
351
399
  // Bind vendor to session on first message (if not already set)
352
400
  if (!session.vendor && msg.vendor) {
@@ -363,7 +411,7 @@ function attachUserMessage(ctx) {
363
411
 
364
412
  // Keep any pending scheduled message alive when user sends a regular message
365
413
 
366
- var userMsg2 = { type: "user_message", text: msg.text || "" };
414
+ var userMsg2 = { type: "user_message", text: msg.text || "", projectSlug: slug, sessionId: session.localId };
367
415
  // Attach sender info for multi-user attribution (backward-compatible: old clients ignore these)
368
416
  if (ws._clayUser) {
369
417
  userMsg2.from = ws._clayUser.id;
@@ -391,24 +439,51 @@ function attachUserMessage(ctx) {
391
439
  }
392
440
  if (deliveryReceipt.clientMessageId) userMsg2.clientMessageId = deliveryReceipt.clientMessageId;
393
441
  if (!userMsg2._ts) userMsg2._ts = Date.now();
394
- session.history.push(userMsg2);
395
- sm.appendToSessionFile(session, userMsg2);
396
- messageDelivery.markRecorded(session, deliveryReceipt, userMsg2);
397
- messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(userMsg2));
398
- sendToSessionOthers(ws, session.localId, hydrateImageRefs(userMsg2));
399
-
400
- if (!session.title) {
401
- session.title = (msg.text || "Image").substring(0, 50);
402
- sm.saveSessionFile(session);
403
- sm.broadcastSessionList();
404
- // Sync auto-title to SDK
405
- if (session.cliSessionId) {
406
- adapter.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
407
- console.error("[project] SDK renameSession failed:", e.message);
408
- });
442
+ var transcriptRecorded = false;
443
+ var pendingContextPreviews = [];
444
+ function recordTranscript() {
445
+ if (transcriptRecorded) return;
446
+ transcriptRecorded = true;
447
+ if (pendingInternal && typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
448
+ if (pendingInternal && gitAttribution) gitAttribution.beginTurn(session);
449
+ session.history.push(userMsg2);
450
+ sm.appendToSessionFile(session, userMsg2);
451
+ messageDelivery.markRecorded(session, deliveryReceipt, userMsg2);
452
+ if (!pendingInternal) messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(userMsg2), session);
453
+ sendToSessionOthers(pendingInternal ? null : ws, session.localId, hydrateImageRefs(userMsg2));
454
+ for (var previewIndex = 0; previewIndex < pendingContextPreviews.length; previewIndex++) recordContextPreview(pendingContextPreviews[previewIndex]);
455
+ recordSessionTitle();
456
+ }
457
+ if (!pendingInternal) recordTranscript();
458
+
459
+ function recordSessionTitle() {
460
+ if (!session.title) {
461
+ session.title = (msg.text || "Image").substring(0, 50);
462
+ sm.saveSessionFile(session);
463
+ sm.broadcastSessionList();
464
+ // Sync auto-title to SDK
465
+ if (session.cliSessionId) {
466
+ adapter.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
467
+ console.error("[project] SDK renameSession failed:", e.message);
468
+ });
469
+ }
409
470
  }
410
471
  }
411
472
 
473
+ function recordContextPreview(previewEntry) {
474
+ session.history.push(previewEntry);
475
+ sendToSession(session.localId, {
476
+ type: "context_preview",
477
+ tab: {
478
+ title: previewEntry.tab.title,
479
+ url: previewEntry.tab.url,
480
+ favIconUrl: previewEntry.tab.favIconUrl,
481
+ screenshotUrl: "/p/" + slug + "/images/" + previewEntry.tab.screenshotFile,
482
+ },
483
+ });
484
+ sm.saveSessionFile(session);
485
+ }
486
+
412
487
  var fullText = msg.text || "";
413
488
  // Prepend saved image paths so Claude can copy/save them
414
489
  if (savedImagePaths.length > 0) {
@@ -423,16 +498,20 @@ function attachUserMessage(ctx) {
423
498
  }
424
499
 
425
500
  // Inject pending @mention context so the current agent sees the exchange
501
+ var pendingMentionCount = 0;
426
502
  if (session.pendingMentionContexts && session.pendingMentionContexts.length > 0) {
427
503
  var mentionPrefix = session.pendingMentionContexts.join("\n\n");
428
- session.pendingMentionContexts = [];
504
+ pendingMentionCount = session.pendingMentionContexts.length;
505
+ if (!pendingInternal) session.pendingMentionContexts = [];
429
506
  fullText = mentionPrefix + "\n\n" + fullText;
430
507
  }
431
508
 
432
509
  // Inject one-shot shell results captured from the composer command mode.
510
+ var pendingShellCount = 0;
433
511
  if (session.pendingShellContexts && session.pendingShellContexts.length > 0) {
434
512
  var shellPrefix = session.pendingShellContexts.join("\n\n");
435
- session.pendingShellContexts = [];
513
+ pendingShellCount = session.pendingShellContexts.length;
514
+ if (!pendingInternal) session.pendingShellContexts = [];
436
515
  fullText = shellPrefix + "\n\n" + fullText;
437
516
  }
438
517
  if (msg.shellCommandResponse) {
@@ -444,6 +523,7 @@ function attachUserMessage(ctx) {
444
523
  var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
445
524
  var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
446
525
  var ctxSources = loadContextSources(slug, session.localId);
526
+ var pendingTermCursors = {};
447
527
  if (ctxSources.length > 0) {
448
528
  if (!session._termContextCursors) session._termContextCursors = {};
449
529
  var termContextParts = [];
@@ -463,7 +543,8 @@ function attachUserMessage(ctx) {
463
543
  lastCursor = Math.max(0, sb.totalBytesWritten - TERM_CONTEXT_MAX);
464
544
  }
465
545
  var newBytes = sb.totalBytesWritten - lastCursor;
466
- session._termContextCursors[termId] = sb.totalBytesWritten;
546
+ if (pendingInternal) pendingTermCursors[termId] = sb.totalBytesWritten;
547
+ else session._termContextCursors[termId] = sb.totalBytesWritten;
467
548
  if (newBytes <= 0) continue;
468
549
  // Build timestamped delta from chunks
469
550
  var deltaChunks = [];
@@ -543,10 +624,51 @@ function attachUserMessage(ctx) {
543
624
  return !!_browserTabList[tid];
544
625
  });
545
626
 
627
+ function pendingBlockReason() {
628
+ if (!pendingInternal) return null;
629
+ if (!pendingMessageQueue.isClaimed(session, pendingInternal.item.id)) return "Pending message was cancelled";
630
+ if (pendingMessageQueue.isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested) return "Pending message dispatch paused";
631
+ if (typeof ctx.authorizePendingDispatch !== "function" || !ctx.authorizePendingDispatch(session, ws && ws._clayUser, pendingInternal.item)) return "Pending message authorization changed";
632
+ return null;
633
+ }
634
+
635
+ function pendingStillAuthorized() {
636
+ return !pendingBlockReason();
637
+ }
638
+
639
+ function acceptPendingDispatch() {
640
+ if (!pendingInternal) return true;
641
+ var blockReason = pendingBlockReason();
642
+ if (blockReason) {
643
+ rejectPendingDispatch(blockReason);
644
+ return false;
645
+ }
646
+ if (pendingMentionCount && Array.isArray(session.pendingMentionContexts)) session.pendingMentionContexts.splice(0, pendingMentionCount);
647
+ if (pendingShellCount && Array.isArray(session.pendingShellContexts)) session.pendingShellContexts.splice(0, pendingShellCount);
648
+ Object.keys(pendingTermCursors).forEach(function (termId) { session._termContextCursors[termId] = pendingTermCursors[termId]; });
649
+ recordTranscript();
650
+ pendingInternal.complete(true);
651
+ return true;
652
+ }
653
+
654
+ function rejectPendingDispatch(error) {
655
+ if (!pendingInternal) return;
656
+ var reason = error && error.message ? error.message : error || "Pending message dispatch was rejected";
657
+ if (reason === "Pending message dispatch paused") pendingInternal.complete("release", reason);
658
+ else pendingInternal.complete(false, reason);
659
+ }
660
+
546
661
  function startFreshQuery(finalText) {
547
662
  session._queryStartTs = Date.now();
548
663
  console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
549
- sdk.startQuery(session, finalText, msg.images, ensureProjectAccessForSession(session));
664
+ var started = sdk.startQuery(session, finalText, msg.images, ensureProjectAccessForSession(session), function () {
665
+ var blockReason = pendingBlockReason();
666
+ if (blockReason) rejectPendingDispatch(blockReason);
667
+ return !blockReason;
668
+ }, acceptPendingDispatch);
669
+ if (pendingInternal && started && typeof started.then === "function") started.then(function (accepted) {
670
+ if (accepted === false) rejectPendingDispatch("Pending message dispatch was rejected");
671
+ }).catch(rejectPendingDispatch);
550
672
  }
551
673
 
552
674
  function dispatchToSdk(finalText) {
@@ -565,16 +687,18 @@ function attachUserMessage(ctx) {
565
687
  // which used to leave the processing indicator bouncing with no
566
688
  // response until the user sent the message again.
567
689
  startFreshQuery(finalText);
568
- }
690
+ } else acceptPendingDispatch();
569
691
  } else if (!sdk.pushMessage(session, finalText, msg.images, autonomousDeliveryMeta)) {
570
692
  // isProcessing was stuck true with no live query: same recovery.
571
693
  startFreshQuery(finalText);
572
- }
694
+ } else acceptPendingDispatch();
573
695
  sm.broadcastSessionList();
574
696
  }
575
697
 
576
698
  // Wait for email context, then proceed with browser tab context and dispatch
577
699
  emailContextPromise.then(function (emailCtxText) {
700
+ var emailBlockReason = pendingBlockReason();
701
+ if (emailBlockReason) { rejectPendingDispatch(emailBlockReason); return; }
578
702
  if (emailCtxText) {
579
703
  fullText = emailCtxText + "\n\n" + fullText;
580
704
  }
@@ -586,6 +710,8 @@ function attachUserMessage(ctx) {
586
710
  return requestTabContext(tabId);
587
711
  });
588
712
  Promise.all(tabPromises).then(function(results) {
713
+ var tabBlockReason = pendingBlockReason();
714
+ if (tabBlockReason) { rejectPendingDispatch(tabBlockReason); return; }
589
715
  var tabContextParts = [];
590
716
  var screenshotImages = [];
591
717
 
@@ -703,38 +829,48 @@ function attachUserMessage(ctx) {
703
829
  screenshotFile: ss.file
704
830
  }
705
831
  };
706
- session.history.push(previewEntry);
707
- // Send context card to all clients
708
- sendToSession(session.localId, {
709
- type: "context_preview",
710
- tab: {
711
- title: ss.tabTitle || "",
712
- url: ss.tabUrl || "",
713
- favIconUrl: ss.tabFavIconUrl || "",
714
- screenshotUrl: "/p/" + slug + "/images/" + ss.file
715
- }
716
- });
832
+ if (pendingInternal) pendingContextPreviews.push(previewEntry);
833
+ else recordContextPreview(previewEntry);
717
834
  // Add to SDK images for multimodal
718
835
  msg.images.push({ mediaType: ss.mediaType, data: ss.data });
719
836
  }
720
- sm.saveSessionFile(session);
721
837
  }
722
838
 
723
839
  dispatchToSdk(fullText);
724
- });
840
+ }).catch(rejectPendingDispatch);
725
841
  } else {
726
842
  dispatchToSdk(fullText);
727
843
  }
728
844
 
729
- }); // emailContextPromise.then
845
+ }).catch(rejectPendingDispatch); // emailContextPromise.then
730
846
 
731
847
  return true;
732
848
  }
733
849
 
850
+ function consumePendingMessage(session) {
851
+ if (!pendingMessageQueue) return false;
852
+ var consumed = pendingMessageQueue.consumeOne(session, function (item, complete) {
853
+ var actor = item.actorId && usersModule.findUserById(item.actorId);
854
+ if ((!actor && item.actorId !== "default") || typeof ctx.authorizePendingDispatch !== "function" || !ctx.authorizePendingDispatch(session, actor || null, item)) {
855
+ complete(false, "Pending message authorization changed");
856
+ return true;
857
+ }
858
+ var source = { _clayUser: actor || null, readyState: 1, send: function () {} };
859
+ handleUserMessage(source, copyMessage(item.message), session, { pendingConsumed: true, item: item, complete: complete });
860
+ return true;
861
+ });
862
+ return !!consumed;
863
+ }
864
+
734
865
  return {
735
866
  handleUserMessage: handleUserMessage,
867
+ consumePendingMessage: consumePendingMessage,
736
868
  syncNotesKnowledge: syncNotesKnowledge,
737
869
  };
738
870
  }
739
871
 
872
+ function copyMessage(message) {
873
+ return JSON.parse(JSON.stringify(message || {}));
874
+ }
875
+
740
876
  module.exports = { attachUserMessage: attachUserMessage };
@@ -31,6 +31,7 @@ function attachProjectWorkspaceQuery(ctx) {
31
31
  function createMcpServer(adapter, session) {
32
32
  if (!service || isMate !== true) return null;
33
33
  var bound = session ? binding(session) : null;
34
+ if (session && !bound) return null;
34
35
  var projectBound = binding(null);
35
36
  if (!projectBound) return null;
36
37
  return workspaceMcp.createMcpServer(adapter, bound, projectBound.isClay);
@@ -39,6 +40,7 @@ function attachProjectWorkspaceQuery(ctx) {
39
40
  function createHistoryMcpServer(adapter, session) {
40
41
  if (!adapter || typeof adapter.createToolServer !== "function") return null;
41
42
  var bound = session ? binding(session) : null;
43
+ if (session && !bound) return null;
42
44
  var projectBound = binding(null);
43
45
  if (!projectBound || projectBound.isClay !== true) return null;
44
46
  return adapter.createToolServer({
@@ -48,6 +50,28 @@ function attachProjectWorkspaceQuery(ctx) {
48
50
  });
49
51
  }
50
52
 
53
+ function getLateSessionMcpServers(adapter, session, startupServers, boundServers) {
54
+ var servers = {};
55
+ if (!session) return servers;
56
+ if (isMate && !Object.prototype.hasOwnProperty.call(startupServers || {}, "clay-workspace") && !boundServers["clay-workspace"]) {
57
+ try {
58
+ var workspace = createMcpServer(adapter, session);
59
+ if (workspace) servers["clay-workspace"] = workspace;
60
+ } catch (e) {
61
+ console.error("[project-workspace-query] Failed to late-bind clay-workspace MCP server:", e.message);
62
+ }
63
+ }
64
+ if (isMate && !Object.prototype.hasOwnProperty.call(startupServers || {}, "clay-history") && !boundServers["clay-history"]) {
65
+ try {
66
+ var history = createHistoryMcpServer(adapter, session);
67
+ if (history) servers["clay-history"] = history;
68
+ } catch (e) {
69
+ console.error("[project-workspace-query] Failed to late-bind clay-history MCP server:", e.message);
70
+ }
71
+ }
72
+ return servers;
73
+ }
74
+
51
75
  function isAuthoritativeClay() {
52
76
  var bound = binding(null);
53
77
  return !!(bound && bound.isClay === true);
@@ -130,6 +154,7 @@ function attachProjectWorkspaceQuery(ctx) {
130
154
  return {
131
155
  createMcpServer: createMcpServer,
132
156
  createHistoryMcpServer: createHistoryMcpServer,
157
+ getLateSessionMcpServers: getLateSessionMcpServers,
133
158
  isAuthoritativeClay: isAuthoritativeClay,
134
159
  getBridgeTools: getBridgeTools,
135
160
  getHistoryBridgeTools: getHistoryBridgeTools,