clay-server 2.23.1-beta.1 → 2.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/project-debate.js +17 -29
- package/lib/project.js +6 -1
- package/lib/public/app.js +7 -21
- package/lib/public/css/debate.css +25 -0
- package/lib/public/index.html +7 -0
- package/lib/public/modules/debate.js +95 -26
- package/lib/sdk-bridge.js +136 -25
- package/lib/sdk-worker.js +1 -0
- package/lib/sessions.js +8 -0
- package/package.json +1 -1
package/lib/project-debate.js
CHANGED
|
@@ -358,18 +358,20 @@ function attachDebate(ctx) {
|
|
|
358
358
|
console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
|
|
359
359
|
startDebateBriefWatcher(session, debate, briefPath);
|
|
360
360
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
361
|
+
// Only show preparing indicator for quick start (standard setup shows skill in real-time)
|
|
362
|
+
if (debate.quickStart) {
|
|
363
|
+
ctx.sendTo(ws, {
|
|
364
|
+
type: "debate_preparing",
|
|
365
|
+
topic: debate.topic,
|
|
366
|
+
moderatorId: debate.moderatorId,
|
|
367
|
+
moderatorName: moderatorProfile.name,
|
|
368
|
+
setupSessionId: debate.setupSessionId,
|
|
369
|
+
panelists: debate.panelists.map(function (p) {
|
|
370
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
371
|
+
return { mateId: p.mateId, name: prof.name };
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
373
375
|
} else if (phase === "reviewing") {
|
|
374
376
|
console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
|
|
375
377
|
ctx.sendTo(ws, {
|
|
@@ -559,6 +561,7 @@ function attachDebate(ctx) {
|
|
|
559
561
|
|
|
560
562
|
// Quick start: moderator mate uses DM conversation context to generate the debate brief directly
|
|
561
563
|
function handleDebateQuickStart(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath) {
|
|
564
|
+
debate.quickStart = true;
|
|
562
565
|
var debateId = debate.debateId;
|
|
563
566
|
|
|
564
567
|
// Create setup session (still needed for session grouping)
|
|
@@ -720,23 +723,8 @@ function attachDebate(ctx) {
|
|
|
720
723
|
// Watch for brief.json in the debate-specific directory
|
|
721
724
|
startDebateBriefWatcher(session, debate, briefPath);
|
|
722
725
|
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
type: "debate_preparing",
|
|
726
|
-
topic: debate.topic,
|
|
727
|
-
moderatorId: debate.moderatorId,
|
|
728
|
-
moderatorName: moderatorProfile.name,
|
|
729
|
-
setupSessionId: setupSession.localId,
|
|
730
|
-
panelists: debate.panelists.map(function (p) {
|
|
731
|
-
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
732
|
-
return { mateId: p.mateId, name: prof.name };
|
|
733
|
-
}),
|
|
734
|
-
};
|
|
735
|
-
// Send directly to the requesting ws (session switch may not have propagated yet)
|
|
736
|
-
ctx.sendTo(ws, preparingMsg);
|
|
737
|
-
// Also broadcast to any other clients on either session
|
|
738
|
-
ctx.sendToSession(session.localId, preparingMsg);
|
|
739
|
-
ctx.sendToSession(setupSession.localId, preparingMsg);
|
|
726
|
+
// Standard setup: no preparing indicator needed because the user
|
|
727
|
+
// sees the skill working in real-time in the setup session.
|
|
740
728
|
|
|
741
729
|
// Start the setup skill session
|
|
742
730
|
setupSession.history.push({ type: "user_message", text: craftingPrompt });
|
package/lib/project.js
CHANGED
|
@@ -3786,7 +3786,8 @@ function createProjectContext(opts) {
|
|
|
3786
3786
|
onProcessingChanged();
|
|
3787
3787
|
session.sentToolResults = {};
|
|
3788
3788
|
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
3789
|
-
if (!session.queryInstance && !session.worker) {
|
|
3789
|
+
if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
|
|
3790
|
+
// No active query (or worker idle between queries): start a new query
|
|
3790
3791
|
sdk.startQuery(session, fullText, msg.images, getLinuxUserForSession(session));
|
|
3791
3792
|
} else {
|
|
3792
3793
|
sdk.pushMessage(session, fullText, msg.images);
|
|
@@ -5505,6 +5506,10 @@ function createProjectContext(opts) {
|
|
|
5505
5506
|
if (session.messageQueue) {
|
|
5506
5507
|
try { session.messageQueue.end(); } catch (e) {}
|
|
5507
5508
|
}
|
|
5509
|
+
if (session.worker) {
|
|
5510
|
+
try { session.worker.kill(); } catch (e) {}
|
|
5511
|
+
session.worker = null;
|
|
5512
|
+
}
|
|
5508
5513
|
// Close all mention SDK sessions to prevent zombie processes
|
|
5509
5514
|
if (session._mentionSessions) {
|
|
5510
5515
|
var mateIds = Object.keys(session._mentionSessions);
|
package/lib/public/app.js
CHANGED
|
@@ -31,7 +31,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
|
|
|
31
31
|
import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
|
|
32
32
|
import { initLongPress } from './modules/longpress.js';
|
|
33
33
|
import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
|
|
34
|
-
import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal,
|
|
34
|
+
import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState } from './modules/debate.js';
|
|
35
35
|
|
|
36
36
|
// --- Base path for multi-project routing ---
|
|
37
37
|
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
@@ -5413,6 +5413,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
5413
5413
|
matesList: function () { return cachedMatesList || []; },
|
|
5414
5414
|
availableBuiltins: function () { return cachedAvailableBuiltins || []; },
|
|
5415
5415
|
currentMateId: function () { return (dmTargetUser && dmTargetUser.isMate) ? dmTargetUser.id : null; },
|
|
5416
|
+
requireSkills: requireSkills,
|
|
5416
5417
|
});
|
|
5417
5418
|
|
|
5418
5419
|
// --- STT module (voice input via Web Speech API) ---
|
|
@@ -6168,30 +6169,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
6168
6169
|
}, cb);
|
|
6169
6170
|
}
|
|
6170
6171
|
|
|
6171
|
-
function requireClayDebateSetup(cb) {
|
|
6172
|
-
requireSkills({
|
|
6173
|
-
title: "Skill Installation Required",
|
|
6174
|
-
reason: "The Debate Setup skill is required to start a debate.",
|
|
6175
|
-
skills: [{ name: "clay-debate-setup", url: "https://github.com/chadbyte/clay-debate-setup", scope: "global" }]
|
|
6176
|
-
}, cb);
|
|
6177
|
-
}
|
|
6178
6172
|
|
|
6179
|
-
// Debate button in mate sidebar
|
|
6173
|
+
// Debate button in mate sidebar (only visible in DM mode)
|
|
6180
6174
|
var debateBtn = document.getElementById("mate-debate-btn");
|
|
6181
6175
|
if (debateBtn) {
|
|
6182
6176
|
debateBtn.addEventListener("click", function () {
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
return { text: m.text, isMate: m.from !== myUserId };
|
|
6188
|
-
});
|
|
6189
|
-
openQuickDebateModal(contextMessages);
|
|
6190
|
-
} else {
|
|
6191
|
-
requireClayDebateSetup(function () {
|
|
6192
|
-
openDebateModal();
|
|
6193
|
-
});
|
|
6194
|
-
}
|
|
6177
|
+
var contextMessages = dmMessageCache.map(function (m) {
|
|
6178
|
+
return { text: m.text, isMate: m.from !== myUserId };
|
|
6179
|
+
});
|
|
6180
|
+
openDebateModal({ dmContext: contextMessages });
|
|
6195
6181
|
});
|
|
6196
6182
|
}
|
|
6197
6183
|
|
|
@@ -910,6 +910,31 @@
|
|
|
910
910
|
line-height: 1.4;
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
+
.debate-skip-setup-row {
|
|
914
|
+
margin-top: 12px;
|
|
915
|
+
padding: 8px 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.debate-toggle-label {
|
|
919
|
+
display: flex;
|
|
920
|
+
align-items: center;
|
|
921
|
+
gap: 8px;
|
|
922
|
+
font-size: 13px;
|
|
923
|
+
color: var(--text-secondary);
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.debate-toggle-label input[type="checkbox"] {
|
|
928
|
+
margin: 0;
|
|
929
|
+
cursor: pointer;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.debate-skip-setup-hint {
|
|
933
|
+
font-size: 11px;
|
|
934
|
+
color: var(--text-muted);
|
|
935
|
+
margin-left: 4px;
|
|
936
|
+
}
|
|
937
|
+
|
|
913
938
|
.debate-modal-footer {
|
|
914
939
|
display: flex;
|
|
915
940
|
justify-content: flex-end;
|
package/lib/public/index.html
CHANGED
|
@@ -1433,6 +1433,13 @@
|
|
|
1433
1433
|
<textarea id="debate-topic-input" rows="2" placeholder="e.g. Should AI development be regulated?"></textarea>
|
|
1434
1434
|
<label class="debate-field-label" for="debate-panel-list">Select panelists</label>
|
|
1435
1435
|
<div id="debate-panel-list" class="debate-panel-list"></div>
|
|
1436
|
+
<div id="debate-skip-setup-row" class="debate-skip-setup-row hidden">
|
|
1437
|
+
<label class="debate-toggle-label">
|
|
1438
|
+
<input type="checkbox" id="debate-skip-setup-toggle">
|
|
1439
|
+
<span>Skip setup</span>
|
|
1440
|
+
<span class="debate-skip-setup-hint">Moderator generates the brief automatically from conversation context</span>
|
|
1441
|
+
</label>
|
|
1442
|
+
</div>
|
|
1436
1443
|
</div>
|
|
1437
1444
|
<div class="debate-modal-footer">
|
|
1438
1445
|
<button id="debate-modal-cancel" class="debate-btn-cancel">Cancel</button>
|
|
@@ -535,7 +535,8 @@ export function isDebateActive() {
|
|
|
535
535
|
var modalEl = null;
|
|
536
536
|
var selectedPanelists = [];
|
|
537
537
|
|
|
538
|
-
export function openDebateModal() {
|
|
538
|
+
export function openDebateModal(opts) {
|
|
539
|
+
opts = opts || {};
|
|
539
540
|
modalEl = document.getElementById("debate-modal");
|
|
540
541
|
if (!modalEl) return;
|
|
541
542
|
|
|
@@ -547,6 +548,46 @@ export function openDebateModal() {
|
|
|
547
548
|
topicInput.focus();
|
|
548
549
|
}
|
|
549
550
|
|
|
551
|
+
// Show/hide skip-setup toggle (only when dmContext is available)
|
|
552
|
+
var skipSetupRow = document.getElementById("debate-skip-setup-row");
|
|
553
|
+
var skipSetupToggle = document.getElementById("debate-skip-setup-toggle");
|
|
554
|
+
if (skipSetupRow) {
|
|
555
|
+
if (opts.dmContext) {
|
|
556
|
+
skipSetupRow.classList.remove("hidden");
|
|
557
|
+
} else {
|
|
558
|
+
skipSetupRow.classList.add("hidden");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (skipSetupToggle) {
|
|
562
|
+
skipSetupToggle.checked = false;
|
|
563
|
+
skipSetupToggle.onchange = function () {
|
|
564
|
+
var topicLabel = modalEl.querySelector('.debate-field-label[for="debate-topic-input"]');
|
|
565
|
+
var reqSpan = topicLabel ? topicLabel.querySelector(".debate-field-req") : null;
|
|
566
|
+
if (skipSetupToggle.checked) {
|
|
567
|
+
if (topicInput) topicInput.placeholder = "Leave blank to auto-detect from conversation...";
|
|
568
|
+
if (reqSpan) reqSpan.style.display = "none";
|
|
569
|
+
} else {
|
|
570
|
+
if (topicInput) topicInput.placeholder = "e.g. Should AI development be regulated?";
|
|
571
|
+
if (reqSpan) reqSpan.style.display = "";
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Build DM context string for quick start
|
|
577
|
+
var dmContextStr = "";
|
|
578
|
+
if (opts.dmContext && opts.dmContext.length) {
|
|
579
|
+
var recent = opts.dmContext.slice(-20);
|
|
580
|
+
var parts = [];
|
|
581
|
+
for (var i = 0; i < recent.length; i++) {
|
|
582
|
+
var m = recent[i];
|
|
583
|
+
var speaker = m.isMate ? "Mate" : "User";
|
|
584
|
+
var text = m.text || "";
|
|
585
|
+
if (text.length > 500) text = text.substring(0, 500) + "...";
|
|
586
|
+
parts.push(speaker + ": " + text);
|
|
587
|
+
}
|
|
588
|
+
dmContextStr = parts.join("\n");
|
|
589
|
+
}
|
|
590
|
+
|
|
550
591
|
// Populate panelist list from mates (exclude current mate = moderator)
|
|
551
592
|
var panelList = document.getElementById("debate-panel-list");
|
|
552
593
|
if (panelList) {
|
|
@@ -584,7 +625,9 @@ export function openDebateModal() {
|
|
|
584
625
|
if (startBtn) {
|
|
585
626
|
startBtn.onclick = function () {
|
|
586
627
|
var topic = topicInput ? topicInput.value.trim() : "";
|
|
587
|
-
|
|
628
|
+
var skipSetup = skipSetupToggle && skipSetupToggle.checked;
|
|
629
|
+
|
|
630
|
+
if (!skipSetup && !topic) {
|
|
588
631
|
topicInput.focus();
|
|
589
632
|
return;
|
|
590
633
|
}
|
|
@@ -593,32 +636,54 @@ export function openDebateModal() {
|
|
|
593
636
|
var currentMateId = ctx.currentMateId ? ctx.currentMateId() : null;
|
|
594
637
|
if (!currentMateId) return;
|
|
595
638
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
639
|
+
var debatePayload = {
|
|
640
|
+
type: "debate_start",
|
|
641
|
+
moderatorId: currentMateId,
|
|
642
|
+
topic: topic || (skipSetup ? "(auto-detect from conversation)" : ""),
|
|
643
|
+
panelists: selectedPanelists.map(function (id) {
|
|
644
|
+
return { mateId: id, role: "", brief: "" };
|
|
645
|
+
}),
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// When skip setup is toggled, send as quick start with DM context
|
|
649
|
+
if (skipSetup) {
|
|
650
|
+
debatePayload.quickStart = true;
|
|
651
|
+
debatePayload.dmContext = dmContextStr;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function sendDebate() {
|
|
655
|
+
// Create a new session first, then send debate_start after switch
|
|
656
|
+
if (ctx.ws) {
|
|
657
|
+
var onMessage = function (evt) {
|
|
658
|
+
try {
|
|
659
|
+
var data = JSON.parse(evt.data);
|
|
660
|
+
if (data.type === "session_switched") {
|
|
661
|
+
ctx.ws.removeEventListener("message", onMessage);
|
|
662
|
+
ctx.ws.send(JSON.stringify(debatePayload));
|
|
663
|
+
}
|
|
664
|
+
} catch (e) {}
|
|
665
|
+
};
|
|
666
|
+
ctx.ws.addEventListener("message", onMessage);
|
|
667
|
+
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
668
|
+
}
|
|
669
|
+
closeDebateModal();
|
|
619
670
|
}
|
|
620
671
|
|
|
621
|
-
|
|
672
|
+
if (skipSetup) {
|
|
673
|
+
// Quick start: no skill needed
|
|
674
|
+
sendDebate();
|
|
675
|
+
} else {
|
|
676
|
+
// Standard: require clay-debate-setup skill first
|
|
677
|
+
if (ctx.requireSkills) {
|
|
678
|
+
ctx.requireSkills({
|
|
679
|
+
title: "Skill Installation Required",
|
|
680
|
+
reason: "The Debate Setup skill is required to start a debate.",
|
|
681
|
+
skills: [{ name: "clay-debate-setup", url: "https://github.com/chadbyte/clay-debate-setup", scope: "global" }]
|
|
682
|
+
}, sendDebate);
|
|
683
|
+
} else {
|
|
684
|
+
sendDebate();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
622
687
|
};
|
|
623
688
|
}
|
|
624
689
|
|
|
@@ -696,6 +761,10 @@ export function closeDebateModal() {
|
|
|
696
761
|
modalEl.classList.add("hidden");
|
|
697
762
|
}
|
|
698
763
|
selectedPanelists = [];
|
|
764
|
+
var skipSetupToggle = document.getElementById("debate-skip-setup-toggle");
|
|
765
|
+
if (skipSetupToggle) skipSetupToggle.checked = false;
|
|
766
|
+
var skipSetupRow = document.getElementById("debate-skip-setup-row");
|
|
767
|
+
if (skipSetupRow) skipSetupRow.classList.add("hidden");
|
|
699
768
|
}
|
|
700
769
|
|
|
701
770
|
// --- Quick Debate: start debate from DM context ---
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -748,6 +748,13 @@ function createSDKBridge(opts) {
|
|
|
748
748
|
worker._readyResolve = resolve;
|
|
749
749
|
});
|
|
750
750
|
|
|
751
|
+
// Resolves when the worker process actually exits.
|
|
752
|
+
// Used to prevent spawning a new worker before the old one finishes
|
|
753
|
+
// flushing SDK session state to disk (race condition on resume).
|
|
754
|
+
worker.exitPromise = new Promise(function(resolve) {
|
|
755
|
+
worker._exitResolve = resolve;
|
|
756
|
+
});
|
|
757
|
+
|
|
751
758
|
// Create Unix socket server
|
|
752
759
|
worker.server = net.createServer(function(connection) {
|
|
753
760
|
worker.connection = connection;
|
|
@@ -827,6 +834,7 @@ function createSDKBridge(opts) {
|
|
|
827
834
|
// guards against stale workers. This covers all exit cases including
|
|
828
835
|
// signal kills (code=null) and normal exits where the IPC query_error
|
|
829
836
|
// was lost due to connection timing.
|
|
837
|
+
console.log("[sdk-bridge] Exit handler: pid=" + (worker.process ? worker.process.pid : "?") + " ready=" + worker.ready + " _queryEnded=" + worker._queryEnded + " _abortSent=" + worker._abortSent + " handlers=" + worker.messageHandlers.length);
|
|
830
838
|
if (code === 0 && !worker.ready) {
|
|
831
839
|
// Worker exited cleanly before sending "ready"
|
|
832
840
|
for (var h = 0; h < worker.messageHandlers.length; h++) {
|
|
@@ -868,6 +876,10 @@ function createSDKBridge(opts) {
|
|
|
868
876
|
}
|
|
869
877
|
}
|
|
870
878
|
cleanupWorker(worker);
|
|
879
|
+
if (worker._exitResolve) {
|
|
880
|
+
worker._exitResolve();
|
|
881
|
+
worker._exitResolve = null;
|
|
882
|
+
}
|
|
871
883
|
});
|
|
872
884
|
});
|
|
873
885
|
|
|
@@ -885,6 +897,7 @@ function createSDKBridge(opts) {
|
|
|
885
897
|
};
|
|
886
898
|
|
|
887
899
|
worker.kill = function() {
|
|
900
|
+
console.log("[sdk-bridge] worker.kill() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
|
|
888
901
|
worker.send({ type: "shutdown" });
|
|
889
902
|
// Force kill after 5 seconds if still alive (gives SDK time to save session)
|
|
890
903
|
setTimeout(function() {
|
|
@@ -903,6 +916,7 @@ function createSDKBridge(opts) {
|
|
|
903
916
|
}
|
|
904
917
|
|
|
905
918
|
function cleanupWorker(worker) {
|
|
919
|
+
console.log("[sdk-bridge] cleanupWorker() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
|
|
906
920
|
if (worker.connection && !worker.connection.destroyed) {
|
|
907
921
|
try { worker.connection.end(); } catch (e) {}
|
|
908
922
|
}
|
|
@@ -919,6 +933,53 @@ function createSDKBridge(opts) {
|
|
|
919
933
|
* Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
|
|
920
934
|
*/
|
|
921
935
|
async function startQueryViaWorker(session, text, images, linuxUser) {
|
|
936
|
+
// Wait for the previous worker to fully exit before spawning a new one.
|
|
937
|
+
// Without this, the new worker may try to resume the SDK session file
|
|
938
|
+
// while the old worker is still flushing it to disk (800ms grace period),
|
|
939
|
+
// causing "no conversation found" and losing all prior context.
|
|
940
|
+
if (session._workerExitPromise) {
|
|
941
|
+
console.log("[sdk-bridge] startQueryViaWorker: waiting for old worker exit, localId=" + session.localId);
|
|
942
|
+
var exitWait = session._workerExitPromise;
|
|
943
|
+
session._workerExitPromise = null;
|
|
944
|
+
await Promise.race([
|
|
945
|
+
exitWait,
|
|
946
|
+
new Promise(function(resolve) { setTimeout(resolve, 3000); }),
|
|
947
|
+
]);
|
|
948
|
+
console.log("[sdk-bridge] startQueryViaWorker: old worker exit wait done");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Ensure the linux user's .claude directories are writable.
|
|
952
|
+
// The daemon runs as root, so any dirs it creates (mkdirSync) will be
|
|
953
|
+
// root-owned. The SDK (running as the linux user) needs write access
|
|
954
|
+
// to create session files, otherwise resume silently fails.
|
|
955
|
+
if (linuxUser) {
|
|
956
|
+
try {
|
|
957
|
+
var osUsersMod0 = require("./os-users");
|
|
958
|
+
var linuxUserHome0 = osUsersMod0.getLinuxUserHome(linuxUser);
|
|
959
|
+
var uid0 = osUsersMod0.getLinuxUserUid(linuxUser);
|
|
960
|
+
if (uid0 != null) {
|
|
961
|
+
var claudeDir = path.join(linuxUserHome0, ".claude");
|
|
962
|
+
var projectSlug0 = (cwd || "").replace(/\//g, "-");
|
|
963
|
+
var projDir = path.join(claudeDir, "projects", projectSlug0);
|
|
964
|
+
// Create the project directory if missing and chown the whole tree
|
|
965
|
+
if (!fs.existsSync(projDir)) {
|
|
966
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
967
|
+
try { require("child_process").execSync("chown -R " + uid0 + " " + JSON.stringify(claudeDir)); } catch (e) {}
|
|
968
|
+
} else {
|
|
969
|
+
// Fix existing directory if root-owned
|
|
970
|
+
try {
|
|
971
|
+
var pstat = fs.statSync(projDir);
|
|
972
|
+
if (pstat.uid !== uid0) {
|
|
973
|
+
require("child_process").execSync("chown " + uid0 + " " + JSON.stringify(projDir));
|
|
974
|
+
}
|
|
975
|
+
} catch (e) {}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
} catch (e) {
|
|
979
|
+
console.log("[sdk-bridge] Dir ownership fix skipped:", e.message);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
922
983
|
// Pre-copy CLI session file BEFORE spawning worker.
|
|
923
984
|
// Must happen before spawn so execSync doesn't block the event loop
|
|
924
985
|
// while worker is alive (which causes ready/exit race conditions).
|
|
@@ -934,10 +995,26 @@ function createSDKBridge(opts) {
|
|
|
934
995
|
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
|
|
935
996
|
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
936
997
|
var dstFile = path.join(dstDir, sessionFileName);
|
|
937
|
-
|
|
998
|
+
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
999
|
+
// Ensure the projects directory is owned by the linux user so the
|
|
1000
|
+
// SDK can create new session files. Without this, mkdirSync creates
|
|
1001
|
+
// root-owned directories and the SDK silently fails to save sessions.
|
|
1002
|
+
if (!fs.existsSync(dstDir)) {
|
|
938
1003
|
fs.mkdirSync(dstDir, { recursive: true });
|
|
1004
|
+
if (uid != null) {
|
|
1005
|
+
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(dstDir)); } catch (e2) {}
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
// Fix ownership of existing directories created by root
|
|
1009
|
+
try {
|
|
1010
|
+
var dirStat = fs.statSync(dstDir);
|
|
1011
|
+
if (uid != null && dirStat.uid !== uid) {
|
|
1012
|
+
require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
|
|
1013
|
+
}
|
|
1014
|
+
} catch (e2) {}
|
|
1015
|
+
}
|
|
1016
|
+
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
939
1017
|
fs.copyFileSync(srcFile, dstFile);
|
|
940
|
-
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
941
1018
|
if (uid != null) {
|
|
942
1019
|
try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
|
|
943
1020
|
}
|
|
@@ -949,17 +1026,31 @@ function createSDKBridge(opts) {
|
|
|
949
1026
|
}
|
|
950
1027
|
}
|
|
951
1028
|
|
|
1029
|
+
// Reuse existing worker if alive, otherwise spawn a new one.
|
|
1030
|
+
// Keeping the worker alive between queries lets the SDK maintain session
|
|
1031
|
+
// state in memory, avoiding disk-based resume that fails on abort.
|
|
952
1032
|
var worker;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1033
|
+
var reusingWorker = false;
|
|
1034
|
+
if (session.worker && session.worker.ready && session.worker.process && !session.worker.process.killed) {
|
|
1035
|
+
worker = session.worker;
|
|
1036
|
+
reusingWorker = true;
|
|
1037
|
+
// Clear old message handlers so they don't fire for the new query
|
|
1038
|
+
worker.messageHandlers = [];
|
|
1039
|
+
worker._queryEnded = false;
|
|
1040
|
+
worker._abortSent = false;
|
|
1041
|
+
console.log("[sdk-bridge] Reusing existing worker pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1042
|
+
} else {
|
|
1043
|
+
try {
|
|
1044
|
+
worker = spawnWorker(linuxUser);
|
|
1045
|
+
session.worker = worker;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
session.isProcessing = false;
|
|
1048
|
+
onProcessingChanged();
|
|
1049
|
+
sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
|
|
1050
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1051
|
+
sm.broadcastSessionList();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
963
1054
|
}
|
|
964
1055
|
|
|
965
1056
|
session.messageQueue = "worker"; // sentinel: messages go via worker IPC
|
|
@@ -969,7 +1060,7 @@ function createSDKBridge(opts) {
|
|
|
969
1060
|
session.pendingElicitations = {};
|
|
970
1061
|
session.streamedText = false;
|
|
971
1062
|
session.responsePreview = "";
|
|
972
|
-
session.abortController = { abort: function() { worker._abortSent = true; worker.send({ type: "abort" }); } };
|
|
1063
|
+
session.abortController = { abort: function() { console.log("[sdk-bridge] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?")); worker._abortSent = true; worker.send({ type: "abort" }); } };
|
|
973
1064
|
|
|
974
1065
|
// Build initial user message content
|
|
975
1066
|
var content = [];
|
|
@@ -1076,6 +1167,7 @@ function createSDKBridge(opts) {
|
|
|
1076
1167
|
break;
|
|
1077
1168
|
|
|
1078
1169
|
case "query_done":
|
|
1170
|
+
console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1079
1171
|
// Mark that we received a proper IPC completion, so the exit
|
|
1080
1172
|
// handler fallback knows not to double-process.
|
|
1081
1173
|
worker._queryEnded = true;
|
|
@@ -1096,6 +1188,7 @@ function createSDKBridge(opts) {
|
|
|
1096
1188
|
break;
|
|
1097
1189
|
|
|
1098
1190
|
case "query_error": {
|
|
1191
|
+
console.log("[sdk-bridge] IPC query_error received, pid=" + (worker.process ? worker.process.pid : "?") + " _fallback=" + !!msg._fallback + " _queryEnded=" + worker._queryEnded + " error=" + (msg.error || "").substring(0, 100));
|
|
1099
1192
|
// Skip fallback errors from exit handler if we already handled the real one
|
|
1100
1193
|
if (msg._fallback && worker._queryEnded) break;
|
|
1101
1194
|
// Mark that we received a proper IPC completion
|
|
@@ -1105,8 +1198,8 @@ function createSDKBridge(opts) {
|
|
|
1105
1198
|
// Only match the exact SDK error, not generic worker stderr
|
|
1106
1199
|
var isSessionNotFound = qerrLower.indexOf("no conversation found with session id") !== -1;
|
|
1107
1200
|
if (isSessionNotFound) {
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1201
|
+
// Clear stale cliSessionId so next message starts a fresh
|
|
1202
|
+
// conversation in the same UI session.
|
|
1110
1203
|
session.cliSessionId = null;
|
|
1111
1204
|
}
|
|
1112
1205
|
if (session.isProcessing) {
|
|
@@ -1218,16 +1311,18 @@ function createSDKBridge(opts) {
|
|
|
1218
1311
|
});
|
|
1219
1312
|
|
|
1220
1313
|
// Wait for worker to be ready, then send query
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1314
|
+
if (!reusingWorker) {
|
|
1315
|
+
try {
|
|
1316
|
+
await worker.readyPromise;
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
session.isProcessing = false;
|
|
1319
|
+
onProcessingChanged();
|
|
1320
|
+
sendAndRecord(session, { type: "error", text: "Worker failed to connect: " + (e.message || e) });
|
|
1321
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1322
|
+
sm.broadcastSessionList();
|
|
1323
|
+
killSessionWorker(session);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1231
1326
|
}
|
|
1232
1327
|
|
|
1233
1328
|
worker.send({
|
|
@@ -1241,10 +1336,15 @@ function createSDKBridge(opts) {
|
|
|
1241
1336
|
}
|
|
1242
1337
|
|
|
1243
1338
|
function cleanupSessionWorker(session, fromWorker) {
|
|
1339
|
+
console.log("[sdk-bridge] cleanupSessionWorker() called, localId=" + session.localId +
|
|
1340
|
+
" fromWorkerPid=" + (fromWorker && fromWorker.process ? fromWorker.process.pid : "none") +
|
|
1341
|
+
" currentWorkerPid=" + (session.worker && session.worker.process ? session.worker.process.pid : "none") +
|
|
1342
|
+
" stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
|
|
1244
1343
|
// If called from a specific worker's exit/error handler, only cleanup if
|
|
1245
1344
|
// that worker is still the session's current worker. Prevents stale
|
|
1246
1345
|
// worker exit events from killing a newer worker.
|
|
1247
1346
|
if (fromWorker && session.worker && session.worker !== fromWorker) {
|
|
1347
|
+
console.log("[sdk-bridge] cleanupSessionWorker: stale worker guard triggered, skipping");
|
|
1248
1348
|
return;
|
|
1249
1349
|
}
|
|
1250
1350
|
session.queryInstance = null;
|
|
@@ -1254,7 +1354,18 @@ function createSDKBridge(opts) {
|
|
|
1254
1354
|
session.pendingPermissions = {};
|
|
1255
1355
|
session.pendingAskUser = {};
|
|
1256
1356
|
session.pendingElicitations = {};
|
|
1357
|
+
// Keep the worker alive between queries so the SDK can maintain session
|
|
1358
|
+
// state in memory. Killing the worker after each query forces resume from
|
|
1359
|
+
// disk, but the SDK may not save the session file on abort, causing
|
|
1360
|
+
// "no conversation found" and losing all conversation history.
|
|
1361
|
+
// The worker is only killed when the UI session is destroyed or on error.
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Force-kill the worker and remove it from the session.
|
|
1365
|
+
// Used when the session is destroyed or on unrecoverable errors.
|
|
1366
|
+
function killSessionWorker(session) {
|
|
1257
1367
|
if (session.worker) {
|
|
1368
|
+
session._workerExitPromise = session.worker.exitPromise;
|
|
1258
1369
|
session.worker.kill();
|
|
1259
1370
|
session.worker = null;
|
|
1260
1371
|
}
|
package/lib/sdk-worker.js
CHANGED
package/lib/sessions.js
CHANGED
|
@@ -427,6 +427,10 @@ function createSessionManager(opts) {
|
|
|
427
427
|
if (session.messageQueue) {
|
|
428
428
|
try { session.messageQueue.end(); } catch(e) {}
|
|
429
429
|
}
|
|
430
|
+
if (session.worker) {
|
|
431
|
+
try { session.worker.kill(); } catch(e) {}
|
|
432
|
+
session.worker = null;
|
|
433
|
+
}
|
|
430
434
|
|
|
431
435
|
if (session.cliSessionId) {
|
|
432
436
|
try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
|
|
@@ -457,6 +461,10 @@ function createSessionManager(opts) {
|
|
|
457
461
|
if (session.messageQueue) {
|
|
458
462
|
try { session.messageQueue.end(); } catch(e) {}
|
|
459
463
|
}
|
|
464
|
+
if (session.worker) {
|
|
465
|
+
try { session.worker.kill(); } catch(e) {}
|
|
466
|
+
session.worker = null;
|
|
467
|
+
}
|
|
460
468
|
if (session.cliSessionId) {
|
|
461
469
|
try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
|
|
462
470
|
}
|