clay-server 4.7.0-beta.2 → 4.7.0-beta.3
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/public/app.js +2 -0
- package/lib/public/css/input.css +27 -0
- package/lib/public/index.html +3 -0
- package/lib/public/modules/app-connection.js +23 -2
- package/lib/public/modules/app-favicon.js +1 -0
- package/lib/public/modules/app-messages.js +2 -1
- package/lib/public/modules/app-rendering.js +16 -10
- package/lib/public/modules/generation-stop.js +24 -0
- package/lib/sdk-bridge.js +17 -3
- package/lib/sessions.js +2 -0
- package/lib/yoke/acp-process-manager.js +25 -15
- package/lib/yoke/acp-query-handle.js +52 -40
- package/lib/yoke/adapters/antigravity.js +25 -1
- package/lib/yoke/adapters/claude.js +25 -7
- package/lib/yoke/adapters/codex.js +55 -24
- package/lib/yoke/adapters/kiro.js +30 -19
- package/lib/yoke/codex-turn-events.js +32 -0
- package/lib/yoke/codex-turn-stop.js +59 -0
- package/lib/yoke/query-abort.js +40 -0
- package/lib/yoke/user-input-guidance.js +11 -0
- package/lib/yoke/user-input.js +1 -1
- package/package.json +1 -1
package/lib/public/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initGenerationStop } from './modules/generation-stop.js';
|
|
1
2
|
import { initIssueLinks } from './modules/clay-issue-links.js';
|
|
2
3
|
import { initIssues, closeIssues } from './modules/issues.js';
|
|
3
4
|
import { initIssuesContext } from './modules/issues-context.js';
|
|
@@ -821,6 +822,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
821
822
|
|
|
822
823
|
// --- Notifications module ---
|
|
823
824
|
initAppNotifications();
|
|
825
|
+
initGenerationStop();
|
|
824
826
|
|
|
825
827
|
// --- Panels module ---
|
|
826
828
|
initPanels();
|
package/lib/public/css/input.css
CHANGED
|
@@ -1717,3 +1717,30 @@ body.tui-composer-active #config-chip-wrap {
|
|
|
1717
1717
|
font-weight: 600;
|
|
1718
1718
|
color: inherit;
|
|
1719
1719
|
}
|
|
1720
|
+
|
|
1721
|
+
/* Stop remains available when Send is occupied by a draft or Worker lock. */
|
|
1722
|
+
#generation-stop-row {
|
|
1723
|
+
max-width: var(--content-width);
|
|
1724
|
+
margin: 6px auto 0;
|
|
1725
|
+
display: flex;
|
|
1726
|
+
justify-content: flex-end;
|
|
1727
|
+
}
|
|
1728
|
+
#generation-stop-row.hidden { display: none; }
|
|
1729
|
+
body:not(.worker-pane-locked) #input-area:has(#send-btn.stop:not(:disabled)) #generation-stop-row { display: none; }
|
|
1730
|
+
#generation-stop {
|
|
1731
|
+
display: inline-flex;
|
|
1732
|
+
align-items: center;
|
|
1733
|
+
gap: 6px;
|
|
1734
|
+
min-height: 36px;
|
|
1735
|
+
padding: 6px 12px;
|
|
1736
|
+
border: 1px solid var(--border);
|
|
1737
|
+
border-radius: 18px;
|
|
1738
|
+
background: var(--sidebar-bg);
|
|
1739
|
+
color: var(--text-secondary);
|
|
1740
|
+
font: inherit;
|
|
1741
|
+
font-size: 12px;
|
|
1742
|
+
cursor: pointer;
|
|
1743
|
+
}
|
|
1744
|
+
#generation-stop:hover:not(:disabled) { color: var(--text); background: var(--code-bg); }
|
|
1745
|
+
#generation-stop:disabled { opacity: 0.5; cursor: default; }
|
|
1746
|
+
#generation-stop .lucide { width: 12px; height: 12px; }
|
package/lib/public/index.html
CHANGED
|
@@ -635,6 +635,9 @@
|
|
|
635
635
|
</div>
|
|
636
636
|
</div>
|
|
637
637
|
</div>
|
|
638
|
+
<div id="generation-stop-row" class="hidden">
|
|
639
|
+
<button id="generation-stop" type="button" aria-label="Stop generating"><i data-lucide="square"></i><span>Stop</span></button>
|
|
640
|
+
</div>
|
|
638
641
|
</div>
|
|
639
642
|
</div>
|
|
640
643
|
<!-- Mobile input-more bottom sheet: attach / image + context sources -->
|
|
@@ -151,7 +151,19 @@ function suspendSocketWatchdogs() {
|
|
|
151
151
|
watchdog.suspend();
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
function refreshBackgroundPane() {
|
|
155
|
+
if (document.hidden || !store.get('paneNeedsSnapshot')) return false;
|
|
156
|
+
store.set({ paneNeedsSnapshot: false });
|
|
157
|
+
if (!store.get('paneMode') || !store.get('paneSessionId') || lifecycle.isOffline()) return false;
|
|
158
|
+
// A fresh socket discards browser-queued deltas and re-pins the exact pane.
|
|
159
|
+
// Server replay supplies current history, status and pending interactions.
|
|
160
|
+
watchdog.resume();
|
|
161
|
+
connect();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
154
165
|
function resumeSocketWatchdogs(reason) {
|
|
166
|
+
if (refreshBackgroundPane()) return;
|
|
155
167
|
var resumed = watchdog.resume();
|
|
156
168
|
var socket = getWs();
|
|
157
169
|
if (socket && socket.readyState === 1 && resumed) startHeartbeat(socket, lifecycle.getEpoch());
|
|
@@ -178,10 +190,13 @@ export function initConnection() {
|
|
|
178
190
|
});
|
|
179
191
|
});
|
|
180
192
|
document.addEventListener("visibilitychange", function () {
|
|
181
|
-
if (document.hidden)
|
|
182
|
-
|
|
193
|
+
if (document.hidden) {
|
|
194
|
+
if (store.get('paneMode') && store.get('paneSessionId')) store.set({ paneNeedsSnapshot: true });
|
|
195
|
+
suspendSocketWatchdogs();
|
|
196
|
+
} else resumeSocketWatchdogs("visible");
|
|
183
197
|
});
|
|
184
198
|
document.addEventListener("freeze", function () {
|
|
199
|
+
if (store.get('paneMode') && store.get('paneSessionId')) store.set({ paneNeedsSnapshot: true });
|
|
185
200
|
suspendSocketWatchdogs();
|
|
186
201
|
});
|
|
187
202
|
document.addEventListener("resume", function () {
|
|
@@ -387,6 +402,12 @@ export function connect() {
|
|
|
387
402
|
|
|
388
403
|
newWs.onmessage = function (event) {
|
|
389
404
|
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
405
|
+
// Hidden pane deltas are replaced by a current server snapshot on return.
|
|
406
|
+
if (store.get('paneMode') && store.get('paneSessionId') && document.hidden) {
|
|
407
|
+
store.set({ paneNeedsSnapshot: true });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (refreshBackgroundPane()) return;
|
|
390
411
|
// Backup: if we're receiving messages, we're connected
|
|
391
412
|
if (!store.get('connected')) {
|
|
392
413
|
setStatus("connected");
|
|
@@ -83,6 +83,7 @@ export function drawFaviconAnimFrame() {
|
|
|
83
83
|
|
|
84
84
|
export function setSendBtnMode(mode) {
|
|
85
85
|
var sendBtn = getSendBtn();
|
|
86
|
+
sendBtn.setAttribute("aria-label", mode === "stop" ? "Stop generating" : "Send");
|
|
86
87
|
if (mode === "stop") {
|
|
87
88
|
sendBtn.disabled = false;
|
|
88
89
|
sendBtn.classList.add("stop");
|
|
@@ -66,7 +66,7 @@ import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerati
|
|
|
66
66
|
import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
|
|
67
67
|
|
|
68
68
|
// --- App module imports ---
|
|
69
|
-
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
69
|
+
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
70
70
|
import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
|
|
71
71
|
import { setStatus } from './app-connection.js';
|
|
72
72
|
import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
@@ -834,6 +834,7 @@ export function processMessage(msg) {
|
|
|
834
834
|
|
|
835
835
|
case "session_switched":
|
|
836
836
|
clearPairResultStatus();
|
|
837
|
+
resetStreamBuffer();
|
|
837
838
|
syncAutonomousRunForSession(msg.autonomousRun || null);
|
|
838
839
|
// Save draft from outgoing session
|
|
839
840
|
var _prevSid = store.get('activeSessionId');
|
|
@@ -45,6 +45,11 @@ export function addCopyHandler(msgEl, rawText) {
|
|
|
45
45
|
export function appendDelta(text) {
|
|
46
46
|
ensureAssistantBlock();
|
|
47
47
|
streamBuffer += text;
|
|
48
|
+
if (store.get('replayingHistory')) {
|
|
49
|
+
store.set({ currentFullText: store.get('currentFullText') + streamBuffer });
|
|
50
|
+
streamBuffer = "";
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
48
53
|
if (!streamDrainTimer) {
|
|
49
54
|
streamDrainTimer = requestAnimationFrame(drainStreamTick);
|
|
50
55
|
}
|
|
@@ -55,16 +60,9 @@ function drainStreamTick() {
|
|
|
55
60
|
var _s = store.snap();
|
|
56
61
|
if (!_s.currentMsgEl || streamBuffer.length === 0) return;
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
var
|
|
60
|
-
|
|
61
|
-
else if (len > 80) { n = 8; }
|
|
62
|
-
else if (len > 30) { n = 5; }
|
|
63
|
-
else if (len > 10) { n = 2; }
|
|
64
|
-
else { n = 1; }
|
|
65
|
-
|
|
66
|
-
var chunk = streamBuffer.slice(0, n);
|
|
67
|
-
streamBuffer = streamBuffer.slice(n);
|
|
63
|
+
// Render everything received by this frame; never replay a typing backlog.
|
|
64
|
+
var chunk = streamBuffer;
|
|
65
|
+
streamBuffer = "";
|
|
68
66
|
var newText = _s.currentFullText + chunk;
|
|
69
67
|
store.set({ currentFullText: newText });
|
|
70
68
|
|
|
@@ -82,6 +80,14 @@ function drainStreamTick() {
|
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
82
|
|
|
83
|
+
export function resetStreamBuffer() {
|
|
84
|
+
if (streamDrainTimer) cancelAnimationFrame(streamDrainTimer);
|
|
85
|
+
if (highlightTimer) clearTimeout(highlightTimer);
|
|
86
|
+
streamDrainTimer = null;
|
|
87
|
+
highlightTimer = null;
|
|
88
|
+
streamBuffer = "";
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
export function flushStreamBuffer() {
|
|
86
92
|
if (streamDrainTimer) { cancelAnimationFrame(streamDrainTimer); streamDrainTimer = null; }
|
|
87
93
|
if (streamBuffer.length > 0) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
|
|
4
|
+
// This control lives outside the composer so drafts and Worker locks cannot
|
|
5
|
+
// take away the human's ability to stop the displayed session.
|
|
6
|
+
export function initGenerationStop() {
|
|
7
|
+
var row = document.getElementById('generation-stop-row');
|
|
8
|
+
var button = document.getElementById('generation-stop');
|
|
9
|
+
if (!row || !button) return;
|
|
10
|
+
function sync() {
|
|
11
|
+
row.classList.toggle('hidden', !store.get('processing'));
|
|
12
|
+
button.disabled = !store.get('connected');
|
|
13
|
+
button.title = store.get('connected') ? 'Stop generating' : 'Reconnect to stop generating';
|
|
14
|
+
}
|
|
15
|
+
button.addEventListener('click', function () {
|
|
16
|
+
var ws = getWs();
|
|
17
|
+
if (!store.get('processing') || !store.get('connected') || !ws || ws.readyState !== 1) return;
|
|
18
|
+
ws.send(JSON.stringify({ type: 'stop' }));
|
|
19
|
+
});
|
|
20
|
+
store.subscribe(function (state, previous) {
|
|
21
|
+
if (state.processing !== previous.processing || state.connected !== previous.connected) sync();
|
|
22
|
+
});
|
|
23
|
+
sync();
|
|
24
|
+
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const crypto = require("crypto");
|
|
2
|
+
var codexQuestionGuidance = require("./yoke/user-input-guidance").codexQuestionGuidance;
|
|
2
3
|
var fs = require("fs");
|
|
3
4
|
var path = require("path");
|
|
4
5
|
var execSync = require("child_process").execSync;
|
|
@@ -1054,6 +1055,10 @@ function createSDKBridge(opts) {
|
|
|
1054
1055
|
console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
|
|
1055
1056
|
try {
|
|
1056
1057
|
for await (var msg of myQueryInstance) {
|
|
1058
|
+
// An old iterator may drain after Stop or after a replacement query starts.
|
|
1059
|
+
if (!ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) continue;
|
|
1060
|
+
if ((session.taskStopRequested || (myAbortController && myAbortController.signal.aborted))
|
|
1061
|
+
&& msg && msg.yokeType !== "error" && msg.yokeType !== "interrupted") continue;
|
|
1057
1062
|
if (msg && msg.yokeType !== "text_delta" && msg.yokeType !== "thinking_delta" && msg.yokeType !== "tool_input_delta") {
|
|
1058
1063
|
console.log("[sdk-bridge] processQueryStream: received event yokeType=" + msg.yokeType);
|
|
1059
1064
|
}
|
|
@@ -1111,6 +1116,7 @@ function createSDKBridge(opts) {
|
|
|
1111
1116
|
&& session.cliSessionId === msg.sessionId
|
|
1112
1117
|
&& (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session)) {
|
|
1113
1118
|
if (codexCatalogCandidate.version) session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
|
|
1119
|
+
if (codexCatalogCandidate.userInputVersion) session.codexUserInputToolCatalogVersion = codexCatalogCandidate.userInputVersion;
|
|
1114
1120
|
if (codexCatalogCandidate.issuesVersion) session.codexIssuesToolCatalogVersion = codexCatalogCandidate.issuesVersion;
|
|
1115
1121
|
sm.saveSessionFile(session);
|
|
1116
1122
|
codexCatalogCandidate = null;
|
|
@@ -1804,8 +1810,10 @@ function createSDKBridge(opts) {
|
|
|
1804
1810
|
var activeLoopInterview = !isDriverOperatedSession(session) && !session.loopInterviewHandoff && !activeRun && !!loopInterview.activeInterviewId(session);
|
|
1805
1811
|
var structuredInputSession = hasScheduleInterviewTool || activeLoopInterview || (!isDriverOperatedSession(session) && session.ralphCraftingMode === true && session.loop && session.loop.role === "crafting");
|
|
1806
1812
|
var requestedUserInputMode = session.debateSetupMode || session.mateCreationMode ? "fallback" : "auto";
|
|
1807
|
-
if (structuredInputSession) requestedUserInputMode = "fallback";
|
|
1813
|
+
if (structuredInputSession || sessionAdapter.vendor === "codex") requestedUserInputMode = "fallback";
|
|
1808
1814
|
var selectedUserInputMode = yoke.userInput.selectMode(sessionAdapter, requestedUserInputMode);
|
|
1815
|
+
var recoverCodexQuestionTools = sessionAdapter.vendor === "codex" && !!session.cliSessionId
|
|
1816
|
+
&& Number(session.codexUserInputToolCatalogVersion || 0) < 1 && queryContextAvailable();
|
|
1809
1817
|
function structuredInputContextAvailable() {
|
|
1810
1818
|
if (!queryContextAvailable()) return false;
|
|
1811
1819
|
// A schedule-triggered fallback shares the schedule feature's live
|
|
@@ -1928,7 +1936,10 @@ function createSDKBridge(opts) {
|
|
|
1928
1936
|
// would strip the driver session down to the two-sentence pair preset.
|
|
1929
1937
|
// Adapters map this to their additive mechanism (claude: preset+append,
|
|
1930
1938
|
// codex/kiro: prepend to the first message).
|
|
1931
|
-
appendSystemPrompt:
|
|
1939
|
+
appendSystemPrompt: [
|
|
1940
|
+
typeof opts.getSessionSystemPrompt === "function" ? opts.getSessionSystemPrompt(session) : "",
|
|
1941
|
+
sessionAdapter.vendor === "codex" && queryContextAvailable() ? codexQuestionGuidance() : "",
|
|
1942
|
+
].filter(Boolean).join("\n\n") || undefined,
|
|
1932
1943
|
model: queryModel,
|
|
1933
1944
|
// Effort is remembered per vendor and clamped to the session vendor's
|
|
1934
1945
|
// supported levels: a global value would leak codex "minimal" into
|
|
@@ -1946,7 +1957,7 @@ function createSDKBridge(opts) {
|
|
|
1946
1957
|
dynamicTools: sessionToolDefs,
|
|
1947
1958
|
// Codex persists dynamic tools with a thread. A stale authorized Driver
|
|
1948
1959
|
// gets this exact-query bridge; its handlers still reauthorize live.
|
|
1949
|
-
sessionMcpServer: recoverCodexIssueTools ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
|
|
1960
|
+
sessionMcpServer: (recoverCodexIssueTools || recoverCodexQuestionTools) ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
|
|
1950
1961
|
callDynamicTool: function (toolName, args) {
|
|
1951
1962
|
var toolDef = findSessionToolDef(toolName);
|
|
1952
1963
|
if (toolDef && typeof toolDef.handler === "function") {
|
|
@@ -2159,6 +2170,9 @@ function createSDKBridge(opts) {
|
|
|
2159
2170
|
if (completeInterviewCatalog || requiredCatalogTools.report_scheduled_task_outcome) {
|
|
2160
2171
|
codexCatalogCandidate = { generation: queryGeneration, version: yoke.sessionTools.CODEX_CATALOG_VERSION };
|
|
2161
2172
|
}
|
|
2173
|
+
if (requiredCatalogTools.ask_user_questions) {
|
|
2174
|
+
codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, { generation: queryGeneration, userInputVersion: 1 });
|
|
2175
|
+
}
|
|
2162
2176
|
if (hasIssueCatalog) {
|
|
2163
2177
|
codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, {
|
|
2164
2178
|
generation: queryGeneration,
|
package/lib/sessions.js
CHANGED
|
@@ -180,6 +180,7 @@ function createSessionManager(opts) {
|
|
|
180
180
|
if (session.ownerId) metaObj.ownerId = session.ownerId;
|
|
181
181
|
if (session.vendor) metaObj.vendor = session.vendor;
|
|
182
182
|
if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
|
|
183
|
+
if (session.codexUserInputToolCatalogVersion) metaObj.codexUserInputToolCatalogVersion = session.codexUserInputToolCatalogVersion;
|
|
183
184
|
if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
|
|
184
185
|
if (session.model) metaObj.model = session.model;
|
|
185
186
|
if (session.effort) metaObj.effort = session.effort;
|
|
@@ -354,6 +355,7 @@ function createSessionManager(opts) {
|
|
|
354
355
|
};
|
|
355
356
|
if (m.vendor) session.vendor = m.vendor;
|
|
356
357
|
session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
|
|
358
|
+
session.codexUserInputToolCatalogVersion = Number(m.codexUserInputToolCatalogVersion || 0);
|
|
357
359
|
session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
|
|
358
360
|
session.model = m.model || deriveSessionModel(loaded[i].history);
|
|
359
361
|
session.effort = m.effort || null;
|
|
@@ -188,33 +188,41 @@ AcpProcessManager.prototype.addRequestHandler = function(method, fn) {
|
|
|
188
188
|
this.requestHandlers[method] = fn;
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
AcpProcessManager.prototype.send = function(method, params, timeoutMs) {
|
|
191
|
+
AcpProcessManager.prototype.send = function(method, params, timeoutMs, signal) {
|
|
192
192
|
var self = this;
|
|
193
193
|
var id = this.nextId++;
|
|
194
194
|
timeoutMs = timeoutMs || 30000;
|
|
195
|
-
|
|
196
195
|
return new Promise(function(resolve, reject) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
var timer = setTimeout(function() {
|
|
196
|
+
var timer;
|
|
197
|
+
function cleanup() {
|
|
198
|
+
if (timer) clearTimeout(timer);
|
|
202
199
|
delete self.pendingRequests[id];
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
if (signal) signal.removeEventListener("abort", cancel);
|
|
201
|
+
}
|
|
202
|
+
function fail(error) { cleanup(); reject(error); }
|
|
203
|
+
function cancel() {
|
|
204
|
+
var error = new Error("ACP request cancelled: " + method);
|
|
205
|
+
error.name = "AbortError";
|
|
206
|
+
fail(error);
|
|
207
|
+
}
|
|
208
|
+
if (signal && signal.aborted) { cancel(); return; }
|
|
209
|
+
if (!self.proc || !self.started) { fail(new Error("ACP server not started")); return; }
|
|
210
|
+
timer = setTimeout(function() { fail(new Error("Request timeout: " + method + " (id=" + id + ")")); }, timeoutMs);
|
|
211
|
+
self.pendingRequests[id] = {
|
|
212
|
+
resolve: function (value) { cleanup(); resolve(value); }, reject: fail, timer: timer,
|
|
213
|
+
};
|
|
214
|
+
if (signal) signal.addEventListener("abort", cancel, { once: true });
|
|
207
215
|
var msg = { jsonrpc: "2.0", id: id, method: method };
|
|
208
216
|
if (params !== undefined) msg.params = params;
|
|
209
|
-
self._write(msg);
|
|
217
|
+
if (self._write(msg) === false) fail(new Error("ACP transport is not writable"));
|
|
210
218
|
});
|
|
211
219
|
};
|
|
212
220
|
|
|
213
221
|
AcpProcessManager.prototype.notify = function(method, params) {
|
|
214
|
-
if (!this.proc || !this.started) return;
|
|
222
|
+
if (!this.proc || !this.started) return false;
|
|
215
223
|
var msg = { jsonrpc: "2.0", method: method };
|
|
216
224
|
if (params !== undefined) msg.params = params;
|
|
217
|
-
this._write(msg);
|
|
225
|
+
return this._write(msg);
|
|
218
226
|
};
|
|
219
227
|
|
|
220
228
|
AcpProcessManager.prototype.respond = function(id, result) {
|
|
@@ -228,11 +236,13 @@ AcpProcessManager.prototype.respondError = function(id, code, message) {
|
|
|
228
236
|
};
|
|
229
237
|
|
|
230
238
|
AcpProcessManager.prototype._write = function(msg) {
|
|
231
|
-
if (!this.proc || !this.proc.stdin || this.proc.stdin.destroyed) return;
|
|
239
|
+
if (!this.proc || !this.proc.stdin || this.proc.stdin.destroyed) return false;
|
|
232
240
|
try {
|
|
233
241
|
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
242
|
+
return true;
|
|
234
243
|
} catch (e) {
|
|
235
244
|
this._error("Write error:", e.message);
|
|
245
|
+
return false;
|
|
236
246
|
}
|
|
237
247
|
};
|
|
238
248
|
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
// Shared ACP Query Handle
|
|
2
2
|
// -----------------------
|
|
3
3
|
// Implements the YOKE QueryHandle contract with standard ACP session methods.
|
|
4
|
-
|
|
5
4
|
var normalizer = require("./acp-event-normalizer");
|
|
6
5
|
var driverRuntime = require("./acp-driver-runtime");
|
|
7
|
-
|
|
6
|
+
var queryAbort = require("./query-abort");
|
|
8
7
|
var uuidCounter = 0;
|
|
9
8
|
function generateUuid(vendor) {
|
|
10
9
|
uuidCounter++;
|
|
11
10
|
return (vendor || "acp") + "-" + Date.now().toString(36) + "-" + uuidCounter.toString(36);
|
|
12
11
|
}
|
|
13
|
-
|
|
14
12
|
function isAuthError(value) {
|
|
15
13
|
return /not logged in|sign in|log ?in|authenticate|authentication|credentials|unauthorized|forbidden|\b401\b/i.test(String(value || ""));
|
|
16
14
|
}
|
|
17
|
-
|
|
18
15
|
function isApproved(decision) {
|
|
19
16
|
return decision === true || (decision && decision.behavior === "allow");
|
|
20
17
|
}
|
|
21
|
-
|
|
22
18
|
function createAcpQueryHandle(acp, queryOpts) {
|
|
23
19
|
queryOpts = queryOpts || {};
|
|
24
20
|
var vendor = queryOpts.vendor || "acp";
|
|
25
21
|
var driver = queryOpts.driver || null;
|
|
26
22
|
var abortController = queryOpts.abortController;
|
|
23
|
+
var cancellation = queryAbort.createAbortScope();
|
|
24
|
+
var removeAbort = function () {};
|
|
25
|
+
var pendingPermissions = new Set();
|
|
26
|
+
var stopDelivery = Promise.resolve();
|
|
27
|
+
function send(method, params, timeout) {
|
|
28
|
+
cancellation.check();
|
|
29
|
+
return cancellation.wait(acp.send(method, params, timeout, cancellation.signal));
|
|
30
|
+
}
|
|
27
31
|
var canUseTool = queryOpts.canUseTool || null;
|
|
28
32
|
var state = normalizer.createEventState({ vendor: vendor, contextWindow: queryOpts.contextWindow });
|
|
29
33
|
state.sessionId = queryOpts.resumeSessionId || null;
|
|
@@ -76,6 +80,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
76
80
|
function endIterator() {
|
|
77
81
|
if (iteratorDone) return;
|
|
78
82
|
iteratorDone = true;
|
|
83
|
+
removeAbort();
|
|
79
84
|
if (eventWaiting) {
|
|
80
85
|
var resolve = eventWaiting;
|
|
81
86
|
eventWaiting = null;
|
|
@@ -128,7 +133,19 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
var answered = false;
|
|
137
|
+
function cancelPermission() {
|
|
138
|
+
if (answered) return;
|
|
139
|
+
answered = true;
|
|
140
|
+
pendingPermissions.delete(cancelPermission);
|
|
141
|
+
acp.respond(msg.id, { outcome: { outcome: "cancelled" } });
|
|
142
|
+
}
|
|
143
|
+
pendingPermissions.add(cancelPermission);
|
|
131
144
|
function respond(allowed) {
|
|
145
|
+
if (answered) return;
|
|
146
|
+
if (isCancelled()) { cancelPermission(); return; }
|
|
147
|
+
answered = true;
|
|
148
|
+
pendingPermissions.delete(cancelPermission);
|
|
132
149
|
var optionId = allowed ? allowId : rejectId;
|
|
133
150
|
if (!optionId) {
|
|
134
151
|
acp.respond(msg.id, { outcome: { outcome: "cancelled" } });
|
|
@@ -160,7 +177,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
160
177
|
input: input,
|
|
161
178
|
options: options,
|
|
162
179
|
}), function() { return { toolName: toolName, input: input }; }) || { toolName: toolName, input: input };
|
|
163
|
-
Promise.resolve(canUseTool(permission.toolName || toolName, permission.input || input, {})).then(function(decision) {
|
|
180
|
+
Promise.resolve(canUseTool(permission.toolName || toolName, permission.input || input, { signal: cancellation.signal })).then(function(decision) {
|
|
164
181
|
respond(isApproved(decision));
|
|
165
182
|
}).catch(function(err) {
|
|
166
183
|
console.error("[yoke/" + vendor + "] canUseTool error:", err.message || err);
|
|
@@ -193,7 +210,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
193
210
|
if (!state.sessionId || !acp.started || value === undefined || value === null) return Promise.resolve();
|
|
194
211
|
var option = configOptionFor(category, fallbackId);
|
|
195
212
|
if (option) {
|
|
196
|
-
return
|
|
213
|
+
return send("session/set_config_option", {
|
|
197
214
|
sessionId: state.sessionId,
|
|
198
215
|
configId: option.id,
|
|
199
216
|
value: value,
|
|
@@ -202,7 +219,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
202
219
|
});
|
|
203
220
|
}
|
|
204
221
|
if (category === "mode") {
|
|
205
|
-
return
|
|
222
|
+
return send("session/set_mode", { sessionId: state.sessionId, modeId: value }, 15000);
|
|
206
223
|
}
|
|
207
224
|
return Promise.reject(new Error("ACP session does not expose configuration option: " + fallbackId));
|
|
208
225
|
}
|
|
@@ -230,7 +247,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
230
247
|
if (safeModeValue([modes.currentModeId])) return Promise.resolve();
|
|
231
248
|
var safeMode = safeModeValue(modes.availableModes || []);
|
|
232
249
|
if (!safeMode) return Promise.reject(new Error("ACP agent started in an unsafe permission mode and exposed no supervised mode"));
|
|
233
|
-
return
|
|
250
|
+
return send("session/set_mode", { sessionId: state.sessionId, modeId: safeMode }, 15000);
|
|
234
251
|
}
|
|
235
252
|
if (driver && driver.permissionModeGuaranteed === true) return Promise.resolve();
|
|
236
253
|
return Promise.reject(new Error("ACP agent did not expose a verifiable supervised permission mode"));
|
|
@@ -302,6 +319,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
function applySessionResult(result) {
|
|
322
|
+
cancellation.check();
|
|
305
323
|
result = result || {};
|
|
306
324
|
if (result.sessionId) state.sessionId = result.sessionId;
|
|
307
325
|
if (Array.isArray(result.configOptions)) state.configOptions = result.configOptions;
|
|
@@ -313,7 +331,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
313
331
|
async function openDefaultSession(handlerEntry) {
|
|
314
332
|
if (state.sessionId && queryOpts.canResumeSession) {
|
|
315
333
|
handlerEntry.sessionId = state.sessionId;
|
|
316
|
-
var resumed = await
|
|
334
|
+
var resumed = await send("session/resume", buildSessionParams("resume", {
|
|
317
335
|
sessionId: state.sessionId,
|
|
318
336
|
cwd: queryOpts.cwd,
|
|
319
337
|
mcpServers: queryOpts.mcpServers || [],
|
|
@@ -324,7 +342,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
324
342
|
handlerEntry.sessionId = state.sessionId;
|
|
325
343
|
state.loadingSession = true;
|
|
326
344
|
try {
|
|
327
|
-
var loaded = await
|
|
345
|
+
var loaded = await send("session/load", buildSessionParams("load", {
|
|
328
346
|
sessionId: state.sessionId,
|
|
329
347
|
cwd: queryOpts.cwd,
|
|
330
348
|
mcpServers: queryOpts.mcpServers || [],
|
|
@@ -336,7 +354,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
336
354
|
}
|
|
337
355
|
|
|
338
356
|
state.sessionId = null;
|
|
339
|
-
var result = await
|
|
357
|
+
var result = await send("session/new", buildSessionParams("new", {
|
|
340
358
|
cwd: queryOpts.cwd,
|
|
341
359
|
mcpServers: queryOpts.mcpServers || [],
|
|
342
360
|
}), 60000);
|
|
@@ -355,7 +373,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
355
373
|
sessionId: state.sessionId,
|
|
356
374
|
prompt: asPrompt(message),
|
|
357
375
|
});
|
|
358
|
-
return
|
|
376
|
+
return send("session/prompt", params, 30 * 60 * 1000);
|
|
359
377
|
});
|
|
360
378
|
}
|
|
361
379
|
|
|
@@ -363,17 +381,17 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
363
381
|
var currentMessage = prependSystemPrompt(initialMessage);
|
|
364
382
|
var handlerEntry = acp.addHandler(handleServerEvent);
|
|
365
383
|
try {
|
|
366
|
-
var sessionResult = await openSession(handlerEntry);
|
|
384
|
+
var sessionResult = await cancellation.wait(openSession(handlerEntry));
|
|
367
385
|
handlerEntry.sessionId = state.sessionId;
|
|
368
|
-
await driverRuntime.callAsync(driver, "afterSessionOpen", driverContext({ sessionResult: sessionResult }), function() {});
|
|
369
|
-
await ensureSafePermissionMode();
|
|
386
|
+
await cancellation.wait(driverRuntime.callAsync(driver, "afterSessionOpen", driverContext({ sessionResult: sessionResult }), function() {}));
|
|
387
|
+
await cancellation.wait(ensureSafePermissionMode());
|
|
370
388
|
if (state.model && state.model !== "auto") await setModel(state.model);
|
|
371
389
|
if (queryOpts.mode) await setConfig("mode", "mode", queryOpts.mode);
|
|
372
390
|
|
|
373
391
|
while (!isCancelled()) {
|
|
374
392
|
normalizer.resetTurnState(state);
|
|
375
393
|
pushEvent({ yokeType: "turn_start", uuid: generateUuid(vendor), messageType: "user" });
|
|
376
|
-
var result = await prompt(currentMessage);
|
|
394
|
+
var result = await cancellation.wait(prompt(currentMessage));
|
|
377
395
|
var closing = normalizer.closeOpenBlocks(state);
|
|
378
396
|
for (var i = 0; i < closing.length; i++) pushEvent(closing[i]);
|
|
379
397
|
if (isCancelled() || (result && result.stopReason === "cancelled")) {
|
|
@@ -395,6 +413,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
395
413
|
: { yokeType: "error", text: message });
|
|
396
414
|
}
|
|
397
415
|
} finally {
|
|
416
|
+
await stopDelivery;
|
|
398
417
|
acp.removeHandler(handlerEntry);
|
|
399
418
|
state.done = true;
|
|
400
419
|
endIterator();
|
|
@@ -448,36 +467,29 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
448
467
|
}; });
|
|
449
468
|
},
|
|
450
469
|
abort: function() {
|
|
470
|
+
if (state.aborted || iteratorDone) return;
|
|
451
471
|
state.aborted = true;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
endIterator();
|
|
472
|
+
cancellation.stop();
|
|
473
|
+
handle.endInput();
|
|
474
|
+
messageQueue = [];
|
|
475
|
+
pendingPermissions.forEach(function (cancel) { try { cancel(); } catch (e) {} });
|
|
476
|
+
stopDelivery = driverRuntime.callAsync(driver, "cancel", driverContext(), function() {
|
|
477
|
+
if (state.sessionId && (!acp.started || acp.notify("session/cancel", { sessionId: state.sessionId }) === false)) {
|
|
478
|
+
throw new Error("ACP transport is disconnected");
|
|
479
|
+
}
|
|
480
|
+
}).catch(function (error) {
|
|
481
|
+
pushEvent({ yokeType: "error", text: "Cancellation could not be delivered: " + error.message });
|
|
482
|
+
}).finally(endIterator);
|
|
483
|
+
return stopDelivery;
|
|
465
484
|
},
|
|
485
|
+
close: function() { handle.endInput(); endIterator(); },
|
|
466
486
|
endInput: function() {
|
|
467
487
|
messageQueueEnded = true;
|
|
468
|
-
if (messageWaiting) {
|
|
469
|
-
var resolve = messageWaiting;
|
|
470
|
-
messageWaiting = null;
|
|
471
|
-
resolve(null);
|
|
472
|
-
}
|
|
488
|
+
if (messageWaiting) { var resolve = messageWaiting; messageWaiting = null; resolve(null); }
|
|
473
489
|
},
|
|
474
490
|
};
|
|
475
491
|
|
|
476
|
-
|
|
477
|
-
abortController.signal.addEventListener("abort", function() {
|
|
478
|
-
if (!state.aborted) handle.abort();
|
|
479
|
-
}, { once: true });
|
|
480
|
-
}
|
|
492
|
+
removeAbort = queryAbort.bindAbort(abortController, function () { handle.abort(); });
|
|
481
493
|
return handle;
|
|
482
494
|
}
|
|
483
495
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var bindAbort = require("../query-abort").bindAbort;
|
|
1
2
|
var childProcess = require("child_process");
|
|
2
3
|
var readline = require("readline");
|
|
3
4
|
var skillDiscovery = require("../skill-discovery");
|
|
@@ -75,6 +76,9 @@ function toolName(name) {
|
|
|
75
76
|
|
|
76
77
|
function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
|
|
77
78
|
var processHandle = null;
|
|
79
|
+
var removeAbort = function () {};
|
|
80
|
+
var killTimer = null;
|
|
81
|
+
var processExited = false;
|
|
78
82
|
var outputReader = null;
|
|
79
83
|
var events = [];
|
|
80
84
|
var eventWaiter = null;
|
|
@@ -115,6 +119,7 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
|
|
|
115
119
|
function endEvents() {
|
|
116
120
|
if (ended) return;
|
|
117
121
|
ended = true;
|
|
122
|
+
removeAbort();
|
|
118
123
|
if (eventWaiter) {
|
|
119
124
|
var resolve = eventWaiter;
|
|
120
125
|
eventWaiter = null;
|
|
@@ -252,6 +257,8 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
|
|
|
252
257
|
endEvents();
|
|
253
258
|
});
|
|
254
259
|
processHandle.on("exit", function(code, signal) {
|
|
260
|
+
processExited = true;
|
|
261
|
+
if (killTimer) clearTimeout(killTimer);
|
|
255
262
|
if (!ended && activeTurn) {
|
|
256
263
|
var message = stderr.trim() || "Antigravity CLI exited before completing the turn";
|
|
257
264
|
pushEvent(isAuthError(message)
|
|
@@ -309,8 +316,24 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
|
|
|
309
316
|
},
|
|
310
317
|
abort: function() {
|
|
311
318
|
if (ended) return;
|
|
319
|
+
inputEnded = true;
|
|
320
|
+
messages = [];
|
|
312
321
|
pushEvent({ yokeType: "interrupted" });
|
|
313
|
-
if (processHandle)
|
|
322
|
+
if (processHandle && !processExited) {
|
|
323
|
+
try { processHandle.kill("SIGINT"); } catch (error) {
|
|
324
|
+
pushEvent({ yokeType: "error", text: "Could not interrupt Antigravity: " + error.message });
|
|
325
|
+
}
|
|
326
|
+
if (!processExited) {
|
|
327
|
+
killTimer = setTimeout(function () {
|
|
328
|
+
if (!processExited) {
|
|
329
|
+
try { processHandle.kill("SIGKILL"); } catch (error) {
|
|
330
|
+
console.error("[yoke/antigravity] Could not terminate stopped query:", error.message);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}, 5000);
|
|
334
|
+
killTimer.unref();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
314
337
|
endEvents();
|
|
315
338
|
},
|
|
316
339
|
close: function() {
|
|
@@ -319,6 +342,7 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
|
|
|
319
342
|
endEvents();
|
|
320
343
|
},
|
|
321
344
|
};
|
|
345
|
+
removeAbort = bindAbort(queryOpts.abortController, function () { handle.abort(); });
|
|
322
346
|
return handle;
|
|
323
347
|
}
|
|
324
348
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// This is the ONLY file (besides claude-worker.js) that imports the SDK.
|
|
5
5
|
// Also manages worker processes for OS-level user isolation.
|
|
6
6
|
|
|
7
|
+
var bindQueryAbort = require("../query-abort").bindAbort;
|
|
7
8
|
var path = require("path");
|
|
8
9
|
var fs = require("fs");
|
|
9
10
|
var os = require("os");
|
|
@@ -576,6 +577,7 @@ function normalizeBackgroundTasks(tasks) {
|
|
|
576
577
|
function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
577
578
|
var correlation = claudeCorrelation.createCorrelation();
|
|
578
579
|
var closed = false;
|
|
580
|
+
var removeAbort = function () {};
|
|
579
581
|
var handle = {
|
|
580
582
|
// Opaque adapter state (null for in-process queries)
|
|
581
583
|
_adapterState: null,
|
|
@@ -586,11 +588,12 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
|
586
588
|
return {
|
|
587
589
|
next: function() {
|
|
588
590
|
return rawIter.next().then(function(result) {
|
|
589
|
-
if (result.done) closed = true;
|
|
591
|
+
if (result.done) { closed = true; removeAbort(); }
|
|
590
592
|
if (result.done) return result;
|
|
591
593
|
return { value: correlation.result(result.value, flattenEvent(result.value)), done: false };
|
|
592
594
|
}, function(err) {
|
|
593
595
|
closed = true;
|
|
596
|
+
removeAbort();
|
|
594
597
|
throw err;
|
|
595
598
|
});
|
|
596
599
|
},
|
|
@@ -684,6 +687,7 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
|
684
687
|
|
|
685
688
|
abort: function() {
|
|
686
689
|
closed = true;
|
|
690
|
+
try { messageQueue.end(); } catch (e) {}
|
|
687
691
|
if (abortController) {
|
|
688
692
|
try { abortController.abort(); } catch (e) {}
|
|
689
693
|
}
|
|
@@ -691,6 +695,7 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
|
691
695
|
|
|
692
696
|
close: function() {
|
|
693
697
|
closed = true;
|
|
698
|
+
removeAbort();
|
|
694
699
|
try { messageQueue.end(); } catch (e) {}
|
|
695
700
|
if (rawQuery && typeof rawQuery.close === "function") {
|
|
696
701
|
try { rawQuery.close(); } catch (e) {}
|
|
@@ -712,6 +717,10 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
|
712
717
|
},
|
|
713
718
|
};
|
|
714
719
|
|
|
720
|
+
removeAbort = bindQueryAbort(abortController, function () {
|
|
721
|
+
closed = true;
|
|
722
|
+
try { messageQueue.end(); } catch (e) {}
|
|
723
|
+
});
|
|
715
724
|
return handle;
|
|
716
725
|
}
|
|
717
726
|
|
|
@@ -1043,6 +1052,7 @@ function serializeWorkerValue(value, seen) {
|
|
|
1043
1052
|
// query identically to an in-process query.
|
|
1044
1053
|
|
|
1045
1054
|
function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool, onUserDialog, onUserInputRequest, onModelSwitch, preToolUsePolicy) {
|
|
1055
|
+
var interactionAbort = new AbortController();
|
|
1046
1056
|
var contextRequests = claudeContextUsage.createRequests(worker);
|
|
1047
1057
|
var correlation = claudeCorrelation.createCorrelation();
|
|
1048
1058
|
// Async iterable state
|
|
@@ -1085,6 +1095,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1085
1095
|
}
|
|
1086
1096
|
|
|
1087
1097
|
function endIter() {
|
|
1098
|
+
interactionAbort.abort();
|
|
1088
1099
|
contextRequests.close();
|
|
1089
1100
|
if (iterEnded) return;
|
|
1090
1101
|
iterEnded = true;
|
|
@@ -1096,6 +1107,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1096
1107
|
}
|
|
1097
1108
|
|
|
1098
1109
|
function errorIter(err) {
|
|
1110
|
+
interactionAbort.abort();
|
|
1099
1111
|
contextRequests.close();
|
|
1100
1112
|
if (iterEnded) return;
|
|
1101
1113
|
iterEnded = true;
|
|
@@ -1122,8 +1134,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1122
1134
|
decisionReason: msg.decisionReason,
|
|
1123
1135
|
defaultToNo: msg.defaultToNo === true,
|
|
1124
1136
|
suppressAlwaysAllowRule: msg.suppressAlwaysAllowRule === true,
|
|
1125
|
-
signal:
|
|
1137
|
+
signal: interactionAbort.signal,
|
|
1126
1138
|
}).then(function(result) {
|
|
1139
|
+
if (interactionAbort.signal.aborted) result = { behavior: "deny", message: "Query cancelled" };
|
|
1127
1140
|
worker.send({ type: "permission_response", requestId: msg.requestId, result: result });
|
|
1128
1141
|
}).catch(function(e) {
|
|
1129
1142
|
console.error("[yoke/claude] permission_response send failed:", e.message || e);
|
|
@@ -1149,8 +1162,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1149
1162
|
if (onUserInputRequest) {
|
|
1150
1163
|
claudeUserInput.ask(onUserInputRequest, msg.input, {
|
|
1151
1164
|
toolUseID: msg.toolUseId,
|
|
1152
|
-
signal:
|
|
1165
|
+
signal: interactionAbort.signal,
|
|
1153
1166
|
}).then(function(result) {
|
|
1167
|
+
if (interactionAbort.signal.aborted) return;
|
|
1154
1168
|
worker.send({ type: "ask_user_response", toolUseId: msg.toolUseId, result: result });
|
|
1155
1169
|
}).catch(function(e) {
|
|
1156
1170
|
console.error("[yoke/claude] ask_user_response send failed:", e.message || e);
|
|
@@ -1171,12 +1185,13 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1171
1185
|
var workerElicitation = onUserInputRequest
|
|
1172
1186
|
? claudeUserInput.elicitation(onUserInputRequest, workerElicitationRequest, {
|
|
1173
1187
|
requestId: msg.requestId,
|
|
1174
|
-
signal:
|
|
1188
|
+
signal: interactionAbort.signal,
|
|
1175
1189
|
})
|
|
1176
1190
|
: onElicitation(workerElicitationRequest, {
|
|
1177
|
-
signal:
|
|
1191
|
+
signal: interactionAbort.signal,
|
|
1178
1192
|
});
|
|
1179
1193
|
workerElicitation.then(function(result) {
|
|
1194
|
+
if (interactionAbort.signal.aborted) return;
|
|
1180
1195
|
worker.send({ type: "elicitation_response", requestId: msg.requestId, result: result });
|
|
1181
1196
|
}).catch(function(e) {
|
|
1182
1197
|
console.error("[yoke/claude] elicitation_response send failed:", e.message || e);
|
|
@@ -1191,8 +1206,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1191
1206
|
payload: msg.payload || {},
|
|
1192
1207
|
toolUseID: msg.toolUseId || undefined,
|
|
1193
1208
|
}, {
|
|
1194
|
-
signal:
|
|
1209
|
+
signal: interactionAbort.signal,
|
|
1195
1210
|
}).then(function(result) {
|
|
1211
|
+
if (interactionAbort.signal.aborted) return;
|
|
1196
1212
|
worker.send({ type: "user_dialog_response", requestId: msg.requestId, result: result });
|
|
1197
1213
|
}).catch(function(e) {
|
|
1198
1214
|
console.error("[yoke/claude] user_dialog_response send failed:", e.message || e);
|
|
@@ -1327,7 +1343,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1327
1343
|
getSubmittedMessageCount: correlation.submitted,
|
|
1328
1344
|
|
|
1329
1345
|
pushMessage: function(text, images) {
|
|
1330
|
-
if (iterEnded || (worker.process && (worker.process.killed || worker.process.exitCode != null))) return false;
|
|
1346
|
+
if (iterEnded || worker._abortSent || (worker.process && (worker.process.killed || worker.process.exitCode != null))) return false;
|
|
1331
1347
|
var content = [];
|
|
1332
1348
|
if (images && images.length > 0) {
|
|
1333
1349
|
for (var i = 0; i < images.length; i++) {
|
|
@@ -1417,8 +1433,10 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1417
1433
|
},
|
|
1418
1434
|
|
|
1419
1435
|
abort: function() {
|
|
1436
|
+
if (iterEnded || worker._abortSent) return;
|
|
1420
1437
|
console.log("[yoke/claude] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1421
1438
|
worker._abortSent = true;
|
|
1439
|
+
interactionAbort.abort();
|
|
1422
1440
|
try { worker.send({ type: "abort" }); } catch (e) {}
|
|
1423
1441
|
// If the worker doesn't finish within 5s (e.g. subagent stuck), force-kill it.
|
|
1424
1442
|
// The worker exit handler will dispatch a fallback query_error and send done.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// Implements the YOKE interface using codex app-server protocol.
|
|
4
4
|
// Bidirectional JSON-RPC over stdin/stdout enables interactive approval flows.
|
|
5
5
|
|
|
6
|
+
var createTurnEvents = require("../codex-turn-events").createTurnEvents;
|
|
7
|
+
var bindQueryAbort = require("../query-abort").bindAbort;
|
|
6
8
|
var path = require("path");
|
|
7
9
|
var fs = require("fs");
|
|
8
10
|
var { CodexAppServer } = require("../codex-app-server");
|
|
@@ -11,6 +13,7 @@ var skillDiscovery = require("../skill-discovery");
|
|
|
11
13
|
var { resolveOsUserInfo } = require("../../os-users");
|
|
12
14
|
var backgroundTasks = require("../codex-background-tasks");
|
|
13
15
|
var codexUserInput = require("../codex-user-input");
|
|
16
|
+
var createTurnStop = require("../codex-turn-stop").createTurnStop;
|
|
14
17
|
|
|
15
18
|
// --- Reasoning defaults ---
|
|
16
19
|
// Reasoning summaries are the only readable thinking channel Codex offers:
|
|
@@ -812,6 +815,7 @@ function createEventState(model) {
|
|
|
812
815
|
|
|
813
816
|
function createCodexQueryHandle(appServer, queryOpts) {
|
|
814
817
|
var abortController = queryOpts.abortController;
|
|
818
|
+
var removeAbort = function () {};
|
|
815
819
|
var promptParts = [queryOpts.systemPrompt, queryOpts.appendSystemPrompt].filter(function (part) { return !!part; });
|
|
816
820
|
var systemPrompt = promptParts.join("\n\n");
|
|
817
821
|
var canUseTool = queryOpts.canUseTool || null;
|
|
@@ -826,6 +830,11 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
826
830
|
}
|
|
827
831
|
|
|
828
832
|
var state = createEventState(queryOpts.model);
|
|
833
|
+
var turnEvents = createTurnEvents(handleServerEvent);
|
|
834
|
+
var turnStop = createTurnStop(appServer, function (error) {
|
|
835
|
+
console.error("[yoke/codex] turn interrupt failed:", error.message || error);
|
|
836
|
+
pushEvent({ yokeType: "error", text: "Codex cancellation could not be confirmed. The remote turn may still be running." });
|
|
837
|
+
});
|
|
829
838
|
|
|
830
839
|
// Internal event buffer for async iterator
|
|
831
840
|
var eventBuffer = [];
|
|
@@ -864,6 +873,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
864
873
|
}
|
|
865
874
|
|
|
866
875
|
function endIterator() {
|
|
876
|
+
removeAbort();
|
|
867
877
|
iteratorDone = true;
|
|
868
878
|
if (eventWaiting) {
|
|
869
879
|
var resolve = eventWaiting;
|
|
@@ -912,6 +922,13 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
912
922
|
// Ignore events from other threads (app-server is shared across sessions)
|
|
913
923
|
if (params.threadId && state.threadId && params.threadId !== state.threadId) return;
|
|
914
924
|
|
|
925
|
+
if (!turnEvents.accept(msg)) return;
|
|
926
|
+
|
|
927
|
+
// Capture cancellation identity even if Stop arrived before turn/start replied.
|
|
928
|
+
var eventTurnId = params.turnId || params.turn_id || (params.turn && params.turn.id);
|
|
929
|
+
if (method === "turn/started") turnStop.started(eventTurnId);
|
|
930
|
+
if (method === "turn/completed" || method === "turn/failed") turnStop.completed(eventTurnId);
|
|
931
|
+
|
|
915
932
|
// After abort, only let turn-ending events through
|
|
916
933
|
if (isCancelled() && method !== "turn/completed" && method !== "turn/failed" && method !== "serverRequest/resolved" && method !== "thread/status/changed") return;
|
|
917
934
|
|
|
@@ -1089,6 +1106,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1089
1106
|
|
|
1090
1107
|
// Resolve turn promise when turn ends
|
|
1091
1108
|
if (method === "turn/completed" || method === "turn/failed") {
|
|
1109
|
+
turnEvents.finish();
|
|
1092
1110
|
if (turnResolve) {
|
|
1093
1111
|
var resolve = turnResolve;
|
|
1094
1112
|
turnResolve = null;
|
|
@@ -1204,6 +1222,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1204
1222
|
while (!isCancelled()) {
|
|
1205
1223
|
// Reset per-turn state
|
|
1206
1224
|
state.turnStarted = false;
|
|
1225
|
+
state.currentTurnId = null;
|
|
1207
1226
|
state.textBlocks = {};
|
|
1208
1227
|
state.textLengths = {};
|
|
1209
1228
|
state.thinkingBlocks = {};
|
|
@@ -1243,11 +1262,22 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1243
1262
|
input = input.concat(skillItemsToInject);
|
|
1244
1263
|
}
|
|
1245
1264
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1265
|
+
turnStop.begin(state.threadId);
|
|
1266
|
+
turnEvents.begin();
|
|
1267
|
+
try {
|
|
1268
|
+
var turnResult = await appServer.send("turn/start", {
|
|
1269
|
+
threadId: state.threadId,
|
|
1270
|
+
input: input,
|
|
1271
|
+
model: state.model,
|
|
1272
|
+
}, 60000);
|
|
1273
|
+
var startedTurnId = turnResult && turnResult.turn && turnResult.turn.id;
|
|
1274
|
+
turnStop.startSettled(startedTurnId);
|
|
1275
|
+
turnEvents.ready(startedTurnId);
|
|
1276
|
+
} catch (startError) {
|
|
1277
|
+
turnEvents.finish();
|
|
1278
|
+
turnStop.startSettled(null);
|
|
1279
|
+
throw startError;
|
|
1280
|
+
}
|
|
1251
1281
|
|
|
1252
1282
|
// Wait for turn to complete
|
|
1253
1283
|
await turnPromise;
|
|
@@ -1348,19 +1378,23 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1348
1378
|
abort: function() {
|
|
1349
1379
|
console.log("[yoke/codex] handle.abort() called, threadId=" + state.threadId + " already aborted=" + state.aborted);
|
|
1350
1380
|
state.aborted = true;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1381
|
+
messageQueueEnded = true;
|
|
1382
|
+
messageQueue = [];
|
|
1383
|
+
if (messageWaiting) {
|
|
1384
|
+
var releaseMessage = messageWaiting;
|
|
1385
|
+
messageWaiting = null;
|
|
1386
|
+
releaseMessage(null);
|
|
1387
|
+
}
|
|
1388
|
+
// Keep the iterator alive until the exact interrupt settles so failures
|
|
1389
|
+
// reach the transcript instead of being silently reported as a stop.
|
|
1390
|
+
return turnStop.abort().then(function () {
|
|
1391
|
+
if (turnResolve) {
|
|
1392
|
+
var resolve = turnResolve;
|
|
1393
|
+
turnResolve = null;
|
|
1394
|
+
resolve();
|
|
1395
|
+
}
|
|
1396
|
+
endIterator();
|
|
1397
|
+
});
|
|
1364
1398
|
},
|
|
1365
1399
|
|
|
1366
1400
|
close: function() {
|
|
@@ -1383,12 +1417,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1383
1417
|
},
|
|
1384
1418
|
};
|
|
1385
1419
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (!state.aborted) handle.abort();
|
|
1390
|
-
}, { once: true });
|
|
1391
|
-
}
|
|
1420
|
+
removeAbort = bindQueryAbort(abortController, function () {
|
|
1421
|
+
if (!state.aborted) handle.abort();
|
|
1422
|
+
});
|
|
1392
1423
|
|
|
1393
1424
|
return handle;
|
|
1394
1425
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// session/request_permission requests for tool approvals.
|
|
13
13
|
// session/cancel (notif) -> interrupt the active turn
|
|
14
14
|
|
|
15
|
+
var queryAbort = require("../query-abort");
|
|
15
16
|
var { execFile } = require("child_process");
|
|
16
17
|
var { KiroAcpServer, findKiroPath } = require("../kiro-acp-server");
|
|
17
18
|
var INITIALIZE_TIMEOUT_MS = require("../interface").INITIALIZE_TIMEOUT_MS;
|
|
@@ -398,6 +399,12 @@ function createEventState(opts) {
|
|
|
398
399
|
|
|
399
400
|
function createKiroQueryHandle(acp, queryOpts) {
|
|
400
401
|
var abortController = queryOpts.abortController;
|
|
402
|
+
var cancellation = queryAbort.createAbortScope();
|
|
403
|
+
var removeAbort = function () {};
|
|
404
|
+
function send(method, params, timeout) {
|
|
405
|
+
cancellation.check();
|
|
406
|
+
return cancellation.wait(acp.send(method, params, timeout, cancellation.signal));
|
|
407
|
+
}
|
|
401
408
|
var promptParts = [queryOpts.systemPrompt, queryOpts.appendSystemPrompt].filter(function (part) { return !!part; });
|
|
402
409
|
var systemPrompt = promptParts.join("\n\n");
|
|
403
410
|
var canUseTool = queryOpts.canUseTool || null;
|
|
@@ -436,6 +443,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
436
443
|
|
|
437
444
|
function endIterator() {
|
|
438
445
|
iteratorDone = true;
|
|
446
|
+
removeAbort();
|
|
439
447
|
if (eventWaiting) {
|
|
440
448
|
var resolve = eventWaiting;
|
|
441
449
|
eventWaiting = null;
|
|
@@ -506,9 +514,10 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
506
514
|
var meta = (tc.toolCallId && state.toolMeta[tc.toolCallId]) || {};
|
|
507
515
|
var toolName = toolNameForKind(tc.kind || meta.kind, tc.title || meta.title);
|
|
508
516
|
var toolInput = tc.rawInput || meta.rawInput || { title: tc.title || meta.title };
|
|
509
|
-
canUseTool(toolName, toolInput, {}).then(function(decision) {
|
|
510
|
-
acp.respond(msg.id, { outcome: { outcome: "selected", optionId: isApproved(decision) ? allowId : rejectId } });
|
|
517
|
+
cancellation.wait(Promise.resolve().then(function () { cancellation.check(); return canUseTool(toolName, toolInput, { signal: cancellation.signal }); })).then(function(decision) {
|
|
518
|
+
acp.respond(msg.id, isCancelled() ? { outcome: { outcome: "cancelled" } } : { outcome: { outcome: "selected", optionId: isApproved(decision) ? allowId : rejectId } });
|
|
511
519
|
}).catch(function(err) {
|
|
520
|
+
if (isCancelled()) { acp.respond(msg.id, { outcome: { outcome: "cancelled" } }); return; }
|
|
512
521
|
console.error("[yoke/kiro] canUseTool error:", err.message);
|
|
513
522
|
acp.respond(msg.id, { outcome: { outcome: "selected", optionId: rejectId } });
|
|
514
523
|
});
|
|
@@ -592,13 +601,13 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
592
601
|
function setSessionModel(model) {
|
|
593
602
|
if (!state.sessionId || !acp.started) return Promise.resolve();
|
|
594
603
|
if (state.engine === "v3") {
|
|
595
|
-
return
|
|
604
|
+
return send("session/set_config_option", {
|
|
596
605
|
sessionId: state.sessionId,
|
|
597
606
|
configId: "model",
|
|
598
607
|
value: model,
|
|
599
608
|
}, 15000);
|
|
600
609
|
}
|
|
601
|
-
return
|
|
610
|
+
return send("session/set_model", { sessionId: state.sessionId, modelId: model }, 15000);
|
|
602
611
|
}
|
|
603
612
|
|
|
604
613
|
// --- Main query loop ---
|
|
@@ -638,12 +647,13 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
638
647
|
handlerEntry.sessionId = state.sessionId;
|
|
639
648
|
state.loadingSession = true;
|
|
640
649
|
try {
|
|
641
|
-
await
|
|
650
|
+
await send("session/load", {
|
|
642
651
|
sessionId: state.sessionId,
|
|
643
652
|
cwd: queryOpts.cwd,
|
|
644
653
|
mcpServers: queryOpts.mcpServers || [],
|
|
645
654
|
}, 60000);
|
|
646
655
|
} catch (e) {
|
|
656
|
+
if (isCancelled()) throw e;
|
|
647
657
|
// If load fails (unknown session), fall back to a fresh session.
|
|
648
658
|
console.warn("[yoke/kiro] session/load failed, starting fresh:", e.message);
|
|
649
659
|
state.sessionId = null;
|
|
@@ -653,7 +663,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
653
663
|
}
|
|
654
664
|
}
|
|
655
665
|
if (!state.sessionId) {
|
|
656
|
-
var newResult = await
|
|
666
|
+
var newResult = await send("session/new", {
|
|
657
667
|
cwd: queryOpts.cwd,
|
|
658
668
|
mcpServers: queryOpts.mcpServers || [],
|
|
659
669
|
}, 60000);
|
|
@@ -664,7 +674,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
664
674
|
// Kiro v3 defaults to autopilot, which bypasses permission requests.
|
|
665
675
|
// Supervised mode is mandatory so every tool call reaches canUseTool.
|
|
666
676
|
if (state.engine === "v3") {
|
|
667
|
-
await
|
|
677
|
+
await send("session/set_config_option", {
|
|
668
678
|
sessionId: state.sessionId,
|
|
669
679
|
configId: "autopilot",
|
|
670
680
|
value: "off",
|
|
@@ -676,7 +686,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
676
686
|
await setSessionModel(queryOpts.model).catch(function() {});
|
|
677
687
|
}
|
|
678
688
|
if (queryOpts.mode) {
|
|
679
|
-
await
|
|
689
|
+
await send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000).catch(function() {});
|
|
680
690
|
}
|
|
681
691
|
|
|
682
692
|
while (!isCancelled()) {
|
|
@@ -690,7 +700,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
690
700
|
// Emit turn_start so the UI records a user turn boundary.
|
|
691
701
|
pushEvent({ yokeType: "turn_start", uuid: generateUuid(), messageType: "user" });
|
|
692
702
|
|
|
693
|
-
var promptResult = await
|
|
703
|
+
var promptResult = await send("session/prompt", {
|
|
694
704
|
sessionId: state.sessionId,
|
|
695
705
|
prompt: input,
|
|
696
706
|
}, 30 * 60 * 1000);
|
|
@@ -779,11 +789,17 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
779
789
|
|
|
780
790
|
abort: function() {
|
|
781
791
|
console.log("[yoke/kiro] handle.abort() sessionId=" + state.sessionId + " already=" + state.aborted);
|
|
792
|
+
if (state.aborted || iteratorDone) return;
|
|
782
793
|
state.aborted = true;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
794
|
+
cancellation.stop();
|
|
795
|
+
handle.endInput();
|
|
796
|
+
messageQueue = [];
|
|
797
|
+
try {
|
|
798
|
+
if (state.sessionId && (!acp.started || acp.notify("session/cancel", { sessionId: state.sessionId }) === false)) {
|
|
799
|
+
throw new Error("ACP transport is disconnected");
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
pushEvent({ yokeType: "error", text: "Kiro cancellation could not be delivered: " + error.message });
|
|
787
803
|
}
|
|
788
804
|
endIterator();
|
|
789
805
|
},
|
|
@@ -808,12 +824,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
808
824
|
},
|
|
809
825
|
};
|
|
810
826
|
|
|
811
|
-
|
|
812
|
-
abortController.signal.addEventListener("abort", function() {
|
|
813
|
-
if (!state.aborted) handle.abort();
|
|
814
|
-
}, { once: true });
|
|
815
|
-
}
|
|
816
|
-
|
|
827
|
+
removeAbort = queryAbort.bindAbort(abortController, function () { handle.abort(); });
|
|
817
828
|
return handle;
|
|
818
829
|
}
|
|
819
830
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// A resumed thread can still deliver notifications from its previous turn.
|
|
2
|
+
// Only turn/start's response authorizes tagged output for this query's turn.
|
|
3
|
+
function createTurnEvents(deliver) {
|
|
4
|
+
var active = false;
|
|
5
|
+
var starting = false;
|
|
6
|
+
var turnId = null;
|
|
7
|
+
var pending = [];
|
|
8
|
+
function idOf(message) {
|
|
9
|
+
var params = message.params || {};
|
|
10
|
+
return params.turnId || params.turn_id || (params.turn && params.turn.id) || null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
begin: function () { active = true; starting = true; turnId = null; pending = []; },
|
|
14
|
+
accept: function (message) {
|
|
15
|
+
var id = idOf(message);
|
|
16
|
+
var scoped = id || /^(turn\/|item\/)/.test(message.method || '');
|
|
17
|
+
if (!scoped) return true;
|
|
18
|
+
if (!active) return false;
|
|
19
|
+
if (starting && id) { pending.push(message); return false; }
|
|
20
|
+
return !id || !turnId || id === turnId;
|
|
21
|
+
},
|
|
22
|
+
ready: function (id) {
|
|
23
|
+
turnId = id;
|
|
24
|
+
starting = false;
|
|
25
|
+
var buffered = pending;
|
|
26
|
+
pending = [];
|
|
27
|
+
buffered.forEach(function (message) { deliver(message); });
|
|
28
|
+
},
|
|
29
|
+
finish: function () { active = false; starting = false; pending = []; },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
module.exports = { createTurnEvents: createTurnEvents };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Stop only the turn owned by this query, including an in-flight turn/start.
|
|
2
|
+
function createTurnStop(server, onError) {
|
|
3
|
+
var turnId = null;
|
|
4
|
+
var threadId = null;
|
|
5
|
+
var starting = false;
|
|
6
|
+
var completed = false;
|
|
7
|
+
var stopped = false;
|
|
8
|
+
var sent = false;
|
|
9
|
+
var resolveStop = null;
|
|
10
|
+
var stopPromise = null;
|
|
11
|
+
|
|
12
|
+
function settle() {
|
|
13
|
+
if (resolveStop) { var resolve = resolveStop; resolveStop = null; resolve(); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function interrupt() {
|
|
17
|
+
if (!stopped || sent) return;
|
|
18
|
+
if (!turnId) { if (!starting) settle(); return; }
|
|
19
|
+
sent = true;
|
|
20
|
+
var target = { threadId: threadId, turnId: turnId };
|
|
21
|
+
Promise.resolve().then(function () {
|
|
22
|
+
if (!server.started) throw new Error('Codex is disconnected; cancellation could not be confirmed.');
|
|
23
|
+
return server.send('turn/interrupt', target, 5000);
|
|
24
|
+
}).catch(function (error) {
|
|
25
|
+
onError(error);
|
|
26
|
+
}).finally(settle);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
begin: function (id) { threadId = id; turnId = null; starting = true; completed = false; sent = false; },
|
|
31
|
+
started: function (id) {
|
|
32
|
+
if (!id || turnId || completed) return;
|
|
33
|
+
turnId = id;
|
|
34
|
+
starting = false;
|
|
35
|
+
interrupt();
|
|
36
|
+
},
|
|
37
|
+
startSettled: function (id) {
|
|
38
|
+
starting = false;
|
|
39
|
+
if (id && !turnId && !completed) turnId = id;
|
|
40
|
+
interrupt();
|
|
41
|
+
},
|
|
42
|
+
completed: function (id) {
|
|
43
|
+
if (id && turnId && id !== turnId) return;
|
|
44
|
+
turnId = null;
|
|
45
|
+
completed = true;
|
|
46
|
+
starting = false;
|
|
47
|
+
if (!sent) settle();
|
|
48
|
+
},
|
|
49
|
+
abort: function () {
|
|
50
|
+
if (stopPromise) return stopPromise;
|
|
51
|
+
stopped = true;
|
|
52
|
+
stopPromise = new Promise(function (resolve) { resolveStop = resolve; });
|
|
53
|
+
interrupt();
|
|
54
|
+
return stopPromise;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { createTurnStop: createTurnStop };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Query-local cancellation. Never terminates a shared provider process.
|
|
2
|
+
function bindAbort(controller, abort) {
|
|
3
|
+
var signal = controller && controller.signal;
|
|
4
|
+
if (!signal) return function () {};
|
|
5
|
+
if (signal.aborted) { abort(); return function () {}; }
|
|
6
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
7
|
+
return function () { signal.removeEventListener('abort', abort); };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createAbortScope() {
|
|
11
|
+
var stopped = false;
|
|
12
|
+
var controller = new AbortController();
|
|
13
|
+
var waiting = new Set();
|
|
14
|
+
function error() { var err = new Error('Query cancelled'); err.name = 'AbortError'; return err; }
|
|
15
|
+
return {
|
|
16
|
+
signal: controller.signal,
|
|
17
|
+
stopped: function () { return stopped; },
|
|
18
|
+
wait: function (promise) {
|
|
19
|
+
return new Promise(function (resolve, reject) {
|
|
20
|
+
function cancel() { reject(error()); }
|
|
21
|
+
if (stopped) cancel();
|
|
22
|
+
else waiting.add(cancel);
|
|
23
|
+
Promise.resolve(promise).then(function (value) {
|
|
24
|
+
waiting.delete(cancel);
|
|
25
|
+
if (stopped) cancel(); else resolve(value);
|
|
26
|
+
}, function (err) { waiting.delete(cancel); reject(err); });
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
stop: function () {
|
|
30
|
+
if (stopped) return;
|
|
31
|
+
stopped = true;
|
|
32
|
+
controller.abort();
|
|
33
|
+
waiting.forEach(function (cancel) { cancel(); });
|
|
34
|
+
waiting.clear();
|
|
35
|
+
},
|
|
36
|
+
check: function () { if (stopped) throw error(); },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { bindAbort: bindAbort, createAbortScope: createAbortScope };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Required decisions use Clay's blocking question lifecycle in every Codex mode.
|
|
2
|
+
function codexQuestionGuidance() {
|
|
3
|
+
return [
|
|
4
|
+
"When clarification materially changes task scope, targets, or intended behavior, use Clay's ask_user_questions tool (or its available MCP-qualified equivalent).",
|
|
5
|
+
"Present the question as a structured input card, not a question followed by bullet choices in ordinary chat. Await the tool result before work that depends on the answer.",
|
|
6
|
+
"Call the blocking question tool by itself; do not batch dependent actions alongside it or detach it into background work. request_user_input may be unavailable outside Plan mode, so use ask_user_questions in that case.",
|
|
7
|
+
"A cancelled, skipped, empty, or failed question is not an answer or approval. Do not infer a choice; leave dependent work pending and explain what answer is needed.",
|
|
8
|
+
"Do not ask again when the user has already answered or authorized the work. This question tool does not replace tool-permission enforcement."
|
|
9
|
+
].join(" ");
|
|
10
|
+
}
|
|
11
|
+
module.exports = { codexQuestionGuidance: codexQuestionGuidance };
|
package/lib/yoke/user-input.js
CHANGED
|
@@ -219,7 +219,7 @@ function fallbackToolDefs(handler, options) {
|
|
|
219
219
|
queryBound: true,
|
|
220
220
|
description: initialLimit === 1
|
|
221
221
|
? "Ask the user exactly one structured question, wait for the answer, then call again if another question is needed. Use zero options for freeform or 2-6 options for choices; never one option."
|
|
222
|
-
: "Ask the user 1-3 structured questions. Use zero options for freeform or 2-6 options for choices; never one option.",
|
|
222
|
+
: "Ask the user 1-3 structured questions and wait for their answers before continuing dependent work. Use this for scope or target decisions; do not substitute prose questions or asynchronous notifications. A cancelled or skipped question is not an answer. Use zero options for freeform or 2-6 options for choices; never one option.",
|
|
223
223
|
inputSchema: buildQuestionShape(initialLimit),
|
|
224
224
|
handler: function (input) {
|
|
225
225
|
if (questionLimit(options) === 1 && input && Array.isArray(input.questions) && input.questions.length !== 1) {
|
package/package.json
CHANGED