clay-server 4.7.0-beta.2 → 4.7.0-beta.4

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.
@@ -387,6 +387,7 @@ function attachPairResultOutbox(options) {
387
387
  markRestartUncertain: markRestartUncertain,
388
388
  authorizeHumanTurn: authorizeHumanTurn,
389
389
  grantManualRetry: grantManualRetry,
390
+ dismissNotice: function (key, revision) { return transition(key, { dismissedNoticeRevision: revision }); },
390
391
  status: status,
391
392
  list: function () { return Object.keys(records).map(function (key) { return copy(records[key]); }); },
392
393
  get: function (key) { return records[key] ? copy(records[key]) : null; },
@@ -146,6 +146,11 @@ function attachPairResultRecovery(ctx) {
146
146
  confirmDuplicate: completed && state === "uncertain",
147
147
  };
148
148
  }
149
+ function noticeItem(record) {
150
+ var item = publicItem(record);
151
+ item.revision = opaqueId(JSON.stringify(item));
152
+ return item;
153
+ }
149
154
  function canRead(ws, session) {
150
155
  if (!ws || !session || ws._clayActiveSession !== session.localId) return false;
151
156
  if (ownerId(session)) return !!(ws._clayUser && String(ws._clayUser.id) === String(ownerId(session)));
@@ -153,7 +158,9 @@ function attachPairResultRecovery(ctx) {
153
158
  }
154
159
  function sendState(session, ws, sendFn) {
155
160
  if (!canRead(ws, session)) return false;
156
- var items = recordsFor(session).map(publicItem);
161
+ var items = recordsFor(session).filter(function (record) {
162
+ return record.dismissedNoticeRevision !== noticeItem(record).revision;
163
+ }).map(noticeItem);
157
164
  (sendFn || ctx.sendTo.bind(null, ws))({ type: "pair_result_status", sessionId: session.localId, items: items });
158
165
  return true;
159
166
  }
@@ -204,7 +211,7 @@ function attachPairResultRecovery(ctx) {
204
211
  changed(session);
205
212
  }
206
213
  function handleMessage(ws, msg) {
207
- if (!msg || (msg.type !== "pair_result_state_request" && msg.type !== "pair_result_retry")) return false;
214
+ if (!msg || (msg.type !== "pair_result_state_request" && msg.type !== "pair_result_retry" && msg.type !== "pair_result_dismiss")) return false;
208
215
  var session = ctx.sm.sessions.get(ws._clayActiveSession);
209
216
  if (!canRead(ws, session)) return true;
210
217
  if (msg.sessionId != null && msg.sessionId !== session.localId) return true;
@@ -213,6 +220,12 @@ function attachPairResultRecovery(ctx) {
213
220
  var records = recordsFor(session), record = null;
214
221
  for (var i = 0; i < records.length; i++) if (opaqueId(records[i].key) === msg.id) record = records[i];
215
222
  if (!record) { sendState(session, ws); return true; }
223
+ if (msg.type === "pair_result_dismiss") {
224
+ if (msg.revision === noticeItem(record).revision) ctx.outbox.dismissNotice(record.key, msg.revision);
225
+ changed(session);
226
+ sendState(session, ws);
227
+ return true;
228
+ }
216
229
  if (record.deliveryState === "uncertain" && msg.confirmDuplicate !== true) { sendState(session, ws); return true; }
217
230
  var granted = ctx.outbox.grantManualRetry(record.key);
218
231
  if (granted.ok) schedule(record.key);
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();
@@ -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; }
@@ -1387,17 +1387,24 @@ pre.mermaid-error {
1387
1387
  .plan-banner.plan-exit { color: var(--success); }
1388
1388
  .plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
1389
1389
 
1390
- .pair-result-status { display: grid; gap: 8px; padding: 10px 16px 0; }
1391
- .pair-result-status-card { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-alt); padding: 10px 12px; color: var(--text-secondary); font-size: 12px; }
1390
+ /* Recovery notices use the same quiet surface as session work references. */
1391
+ .pair-result-status { flex: 0 0 auto; margin: 8px 16px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-alt); color: var(--text-muted); font-size: 12px; line-height: 1.5; max-height: 35vh; overflow-y: auto; }
1392
+ .pair-result-status-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--text-secondary); }
1393
+ .pair-result-status button { font: inherit; color: var(--text-secondary); background: transparent; border: 1px solid var(--border); border-radius: 5px; cursor: pointer; }
1394
+ .pair-result-status .pair-result-status-dismiss { display: grid; place-items: center; flex: 0 0 28px; width: 28px; height: 28px; padding: 0; border: 0; font-size: 20px; line-height: 1; }
1395
+ .pair-result-status button:hover { background: var(--input-bg); color: var(--text); }
1396
+ .pair-result-status button:focus-visible, .pair-result-status summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1397
+ .pair-result-status-details > summary { cursor: pointer; width: fit-content; }
1398
+ .pair-result-status-card { padding: 10px 0; border-top: 1px solid var(--border); margin-top: 8px; }
1392
1399
  .pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
1393
- .pair-result-status-card > strong { color: var(--text); margin-bottom: 3px; }
1400
+ .pair-result-status-card > strong { color: var(--text-secondary); font-weight: 500; margin-bottom: 3px; }
1394
1401
  .pair-result-status-card details { margin-top: 7px; }
1395
- .pair-result-status-card summary { cursor: pointer; color: var(--accent); }
1396
- .pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; font: inherit; }
1397
- .pair-result-status-card button { margin-top: 8px; }
1398
- .pair-result-status-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1402
+ .pair-result-status-card summary { cursor: pointer; }
1403
+ .pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
1404
+ .pair-result-status-retry { margin-top: 8px; padding: 4px 8px; }
1399
1405
  .pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
1400
- .pair-result-status-uncertain, .pair-result-status-blocked { border-color: color-mix(in srgb, var(--warning) 45%, var(--border)); }
1406
+ @media (max-width: 600px) { .pair-result-status { margin: 6px 8px 0; } .pair-result-status .pair-result-status-dismiss { min-width: 36px; height: 36px; } }
1407
+
1401
1408
 
1402
1409
  /* --- Plan card --- */
1403
1410
  .plan-card {
@@ -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) suspendSocketWatchdogs();
182
- else resumeSocketWatchdogs("visible");
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
- var n;
59
- var len = streamBuffer.length;
60
- if (len > 200) { n = Math.ceil(len / 4); }
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
+ }
@@ -33,6 +33,16 @@ function retry(item) {
33
33
  sendRetry(source, false);
34
34
  }
35
35
 
36
+ function dismiss(items) {
37
+ var ws = getWs();
38
+ if (!ws || ws.readyState !== 1) return;
39
+ items.forEach(function (item) {
40
+ ws.send(JSON.stringify({ type: "pair_result_dismiss", id: item.id, revision: item.revision,
41
+ sessionId: store.get("activeSessionId"),
42
+ projectSlug: store.get("currentSlug") || store.get("activeProjectSlug") || null }));
43
+ });
44
+ }
45
+
36
46
  function renderPairResultStatus() {
37
47
  var existing = document.getElementById("pair-result-status");
38
48
  if (existing) existing.remove();
@@ -44,6 +54,21 @@ function renderPairResultStatus() {
44
54
  section.id = "pair-result-status";
45
55
  section.className = "pair-result-status";
46
56
  section.setAttribute("role", "status");
57
+ var header = document.createElement("div");
58
+ header.className = "pair-result-status-header";
59
+ var heading = document.createElement("span");
60
+ heading.textContent = items.length === 1 ? "Worker update needs attention" : items.length + " Worker updates need attention";
61
+ var close = document.createElement("button");
62
+ close.type = "button"; close.className = "pair-result-status-dismiss";
63
+ close.textContent = "×";
64
+ close.setAttribute("aria-label", "Dismiss Worker notices");
65
+ close.title = "Dismiss notices. Retained tasks and results are kept.";
66
+ close.addEventListener("click", dismiss.bind(null, items));
67
+ header.appendChild(heading); header.appendChild(close); section.appendChild(header);
68
+ var content = document.createElement("details"), toggle = document.createElement("summary");
69
+ content.className = "pair-result-status-details";
70
+ toggle.textContent = "Review updates";
71
+ content.appendChild(toggle); section.appendChild(content);
47
72
  for (var i = 0; i < items.length; i++) {
48
73
  var item = items[i], card = document.createElement("div");
49
74
  card.className = "pair-result-status-card pair-result-status-" + item.state;
@@ -61,11 +86,11 @@ function renderPairResultStatus() {
61
86
  }
62
87
  if (item.canRetry) {
63
88
  var button = document.createElement("button");
64
- button.type = "button"; button.className = "confirm-btn confirm-ok pair-result-status-retry"; button.textContent = "Retry delivery";
89
+ button.type = "button"; button.className = "pair-result-status-retry"; button.textContent = "Retry delivery";
65
90
  button.addEventListener("click", retry.bind(null, item));
66
91
  card.appendChild(button);
67
92
  }
68
- section.appendChild(card);
93
+ content.appendChild(card);
69
94
  }
70
95
  messages.parentElement.insertBefore(section, messages);
71
96
  }
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: typeof opts.getSessionSystemPrompt === "function" ? (opts.getSessionSystemPrompt(session) || undefined) : undefined,
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
- if (!self.proc || !self.started) {
198
- reject(new Error("ACP server not started"));
199
- return;
200
- }
201
- var timer = setTimeout(function() {
196
+ var timer;
197
+ function cleanup() {
198
+ if (timer) clearTimeout(timer);
202
199
  delete self.pendingRequests[id];
203
- reject(new Error("Request timeout: " + method + " (id=" + id + ")"));
204
- }, timeoutMs);
205
- self.pendingRequests[id] = { resolve: resolve, reject: reject, timer: timer };
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