clay-server 4.7.0-beta.1 → 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.
Files changed (43) hide show
  1. package/lib/project-connection.js +0 -14
  2. package/lib/project-sessions.js +0 -18
  3. package/lib/public/app.js +16 -14
  4. package/lib/public/css/input.css +27 -0
  5. package/lib/public/css/pane.css +0 -1
  6. package/lib/public/css/projects-hub.css +75 -0
  7. package/lib/public/css/tui-attention.css +0 -407
  8. package/lib/public/index.html +6 -14
  9. package/lib/public/modules/app-connection.js +23 -2
  10. package/lib/public/modules/app-favicon.js +1 -0
  11. package/lib/public/modules/app-home-hub.js +8 -3
  12. package/lib/public/modules/app-messages.js +2 -16
  13. package/lib/public/modules/app-projects.js +16 -11
  14. package/lib/public/modules/app-rendering.js +16 -10
  15. package/lib/public/modules/generation-stop.js +24 -0
  16. package/lib/public/modules/home-surface-boot.js +10 -3
  17. package/lib/public/modules/home-surface.js +13 -1
  18. package/lib/public/modules/projects-hub-view.js +62 -0
  19. package/lib/public/modules/projects-hub.js +105 -0
  20. package/lib/public/style.css +2 -0
  21. package/lib/sdk-bridge.js +17 -3
  22. package/lib/server.js +1 -1
  23. package/lib/sessions.js +2 -0
  24. package/lib/users-home-surface-preferences.js +4 -2
  25. package/lib/users-preferences.js +0 -36
  26. package/lib/users.js +0 -4
  27. package/lib/ws-schema.js +0 -3
  28. package/lib/yoke/acp-process-manager.js +25 -15
  29. package/lib/yoke/acp-query-handle.js +52 -40
  30. package/lib/yoke/adapters/antigravity.js +25 -1
  31. package/lib/yoke/adapters/claude.js +25 -7
  32. package/lib/yoke/adapters/codex.js +55 -24
  33. package/lib/yoke/adapters/kiro.js +30 -19
  34. package/lib/yoke/codex-turn-events.js +32 -0
  35. package/lib/yoke/codex-turn-stop.js +59 -0
  36. package/lib/yoke/query-abort.js +40 -0
  37. package/lib/yoke/user-input-guidance.js +11 -0
  38. package/lib/yoke/user-input.js +1 -1
  39. package/package.json +1 -1
  40. package/lib/public/modules/whats-new-article.js +0 -70
  41. package/lib/public/modules/whats-new.js +0 -264
  42. package/lib/whats-new-content.js +0 -84
  43. package/lib/whats-new.js +0 -57
@@ -0,0 +1,105 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { hideHomeHub, showHomeHub } from './app-home-hub.js';
4
+ import { openAddProjectModal, switchProject } from './app-projects.js';
5
+ import { openUserSettings } from './user-settings.js';
6
+ import { rememberHomePrimarySurface, updateHomeSurfacePreference } from './home-surface.js';
7
+ import { projectsHubContent } from './projects-hub-view.js';
8
+ import { refreshIcons } from './icons.js';
9
+
10
+ function root() { return document.getElementById('projects-hub'); }
11
+
12
+ function render() {
13
+ var element = root();
14
+ if (!element || !store.get('projectsHubVisible')) return;
15
+ var html = projectsHubContent(store.snap());
16
+ if (store.get('projectsHubMarkup') === html) return;
17
+ store.set({ projectsHubMarkup: html });
18
+ var active = element.contains(document.activeElement) ? document.activeElement : null;
19
+ var action = active && (active.dataset.projectsAction || active.dataset.projectsMode || active.dataset.projectSlug);
20
+ element.innerHTML = html;
21
+ refreshIcons();
22
+ if (action) {
23
+ var buttons = element.querySelectorAll('button, a');
24
+ for (var i = 0; i < buttons.length; i++) {
25
+ if ((buttons[i].dataset.projectsAction || buttons[i].dataset.projectsMode || buttons[i].dataset.projectSlug) === action && !buttons[i].disabled) {
26
+ buttons[i].focus({ preventScroll: true });
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ function focusHeading() {
34
+ if (root()) root().scrollTop = 0;
35
+ var title = document.getElementById('projects-title');
36
+ if (title) title.focus({ preventScroll: true });
37
+ }
38
+
39
+ export function showProjectsHub(fromHistory) {
40
+ hideHomeHub();
41
+ store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
42
+ document.body.classList.add('projects-active');
43
+ root().classList.remove('hidden');
44
+ rememberHomePrimarySurface('projects');
45
+ if (!fromHistory && location.pathname !== '/projects') {
46
+ var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
47
+ history[method](null, '', '/projects');
48
+ }
49
+ render();
50
+ focusHeading();
51
+ }
52
+
53
+ export function hideProjectsHub() {
54
+ if (!store.get('projectsHubVisible')) return;
55
+ store.set({ projectsHubVisible: false, projectsShowGuide: false });
56
+ document.body.classList.remove('projects-active');
57
+ if (root()) root().classList.add('hidden');
58
+ }
59
+
60
+ export function completeProjectsOnboarding() {
61
+ if (store.get('onboardingStep') !== 'project') return;
62
+ updateHomeSurfacePreference({ onboardingStep: 'complete' });
63
+ }
64
+
65
+ function handleClick(event) {
66
+ var button = event.target.closest('[data-projects-action], [data-projects-mode], [data-project-slug]');
67
+ if (!button || button.disabled) return;
68
+ event.preventDefault();
69
+ var action = button.dataset.projectsAction;
70
+ if (action === 'home') { showHomeHub(); return; }
71
+ if (action === 'settings') { openUserSettings(); return; }
72
+ if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
73
+ if (action === 'start' || action === 'skip') {
74
+ store.set({ projectsShowGuide: false });
75
+ updateHomeSurfacePreference({ onboardingStep: 'project' });
76
+ if (action === 'skip') showHomeHub();
77
+ else { render(); focusHeading(); }
78
+ return;
79
+ }
80
+ var ws = getWs();
81
+ if (!ws || ws.readyState !== 1) return;
82
+ if (button.dataset.projectSlug) { switchProject(button.dataset.projectSlug); return; }
83
+ if (button.dataset.projectsMode) {
84
+ var permissions = store.get('permissions');
85
+ if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
86
+ openAddProjectModal(button.dataset.projectsMode);
87
+ }
88
+ }
89
+
90
+ export function initProjectsHub() {
91
+ root().addEventListener('click', handleClick);
92
+ store.subscribe(function (state, previous) {
93
+ if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
94
+ showProjectsHub(true);
95
+ if (location.pathname === '/') history.replaceState(null, '', '/projects');
96
+ store.set({ projectsHubRestoreRequested: false });
97
+ return;
98
+ }
99
+ if (!state.projectsHubVisible) return;
100
+ if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
101
+ state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
102
+ state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
103
+ state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
104
+ });
105
+ }
@@ -57,3 +57,5 @@
57
57
  @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
58
58
  @import url("css/pwa-mobile.css?v=20260824f");
59
59
  @import url("css/avatar-imprints.css?v=20260829-imprints1");
60
+
61
+ @import url("css/projects-hub.css");
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/server.js CHANGED
@@ -814,7 +814,7 @@ function createServer(opts) {
814
814
 
815
815
  // Root path — render the default project workspace while keeping Home
816
816
  // available as an explicit client-side surface.
817
- if (fullUrl === "/" && req.method === "GET") {
817
+ if ((fullUrl === "/" || fullUrl === "/projects" || fullUrl === "/projects/") && req.method === "GET") {
818
818
  if (!isRequestAuthed(req)) {
819
819
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
820
820
  res.end(auth.getAuthPage());
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;
@@ -31,8 +31,8 @@ function attachHomeSurfacePreferences(deps) {
31
31
  if (!validId(mateIds[i]) || !validSessionReference(sessions[mateIds[i]])) continue;
32
32
  normalizedSessions[mateIds[i]] = sessions[mateIds[i]];
33
33
  }
34
- return {
35
- surface: source.surface === "home" || source.surface === "project" ? source.surface : null,
34
+ var result = {
35
+ surface: source.surface === "home" || source.surface === "project" || source.surface === "projects" ? source.surface : null,
36
36
  projectSlug: validProjectSlug(source.projectSlug) ? source.projectSlug : null,
37
37
  activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
38
38
  activeSessionByMate: normalizedSessions,
@@ -41,6 +41,8 @@ function attachHomeSurfacePreferences(deps) {
41
41
  chatScope: source.chatScope === "current" ? "current" : "all",
42
42
  subSurface: source.subSurface === "debates" ? "debates" : "chat",
43
43
  };
44
+ if (["welcome", "project", "complete"].indexOf(source.onboardingStep) !== -1) result.onboardingStep = source.onboardingStep;
45
+ return result;
44
46
  }
45
47
 
46
48
  function loadSingleUserPreference() {
@@ -484,40 +484,6 @@ function attachPreferences(deps) {
484
484
  return { error: "User not found" };
485
485
  }
486
486
 
487
- // --- What's New seen ids ---
488
- //
489
- // Per-user list of dismissed "What's New" entry ids. The whats-new
490
- // service uses this to filter unseen entries on connect. Ids are stable
491
- // strings authored in lib/whats-new-content.js.
492
-
493
- function getWhatsNewSeenIds(userId) {
494
- var data = loadUsers();
495
- for (var i = 0; i < data.users.length; i++) {
496
- if (data.users[i].id === userId) {
497
- var ids = data.users[i].whatsNewSeenIds;
498
- return Array.isArray(ids) ? ids.slice() : [];
499
- }
500
- }
501
- return [];
502
- }
503
-
504
- function markWhatsNewSeen(userId, entryId) {
505
- if (!entryId || typeof entryId !== "string") return { error: "bad_id" };
506
- var data = loadUsers();
507
- for (var i = 0; i < data.users.length; i++) {
508
- if (data.users[i].id === userId) {
509
- var ids = Array.isArray(data.users[i].whatsNewSeenIds) ? data.users[i].whatsNewSeenIds.slice() : [];
510
- if (ids.indexOf(entryId) === -1) {
511
- ids.push(entryId);
512
- data.users[i].whatsNewSeenIds = ids;
513
- saveUsers(data);
514
- }
515
- return { ok: true, seenIds: ids };
516
- }
517
- }
518
- return { error: "User not found" };
519
- }
520
-
521
487
  // --- Mate onboarding ---
522
488
 
523
489
  function setMateOnboarded(userId) {
@@ -556,8 +522,6 @@ function attachPreferences(deps) {
556
522
  setMatesEnabled: setMatesEnabled,
557
523
  getToolPalettes: getToolPalettes,
558
524
  setToolPalette: setToolPalette,
559
- getWhatsNewSeenIds: getWhatsNewSeenIds,
560
- markWhatsNewSeen: markWhatsNewSeen,
561
525
  getTerminalFont: getTerminalFont,
562
526
  setTerminalFont: setTerminalFont,
563
527
  setMateOnboarded: setMateOnboarded,
package/lib/users.js CHANGED
@@ -453,8 +453,6 @@ var getMatesEnabled = preferences.getMatesEnabled;
453
453
  var setMatesEnabled = preferences.setMatesEnabled;
454
454
  var getToolPalettes = preferences.getToolPalettes;
455
455
  var setToolPalette = preferences.setToolPalette;
456
- var getWhatsNewSeenIds = preferences.getWhatsNewSeenIds;
457
- var markWhatsNewSeen = preferences.markWhatsNewSeen;
458
456
  var getTerminalFont = preferences.getTerminalFont;
459
457
  var setTerminalFont = preferences.setTerminalFont;
460
458
  var setMateOnboarded = preferences.setMateOnboarded;
@@ -541,8 +539,6 @@ module.exports = {
541
539
  setCapsulesEnabled: setCapsulesEnabled,
542
540
  getToolPalettes: getToolPalettes,
543
541
  setToolPalette: setToolPalette,
544
- getWhatsNewSeenIds: getWhatsNewSeenIds,
545
- markWhatsNewSeen: markWhatsNewSeen,
546
542
  getTerminalFont: getTerminalFont,
547
543
  setTerminalFont: setTerminalFont,
548
544
  getHomeDockPreference: getHomeDockPreference,
package/lib/ws-schema.js CHANGED
@@ -332,9 +332,6 @@ var schema = {
332
332
  // -----------------------------------------------------------------------
333
333
  // What's New
334
334
  // -----------------------------------------------------------------------
335
- "whats_new_state": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Unseen What's New entries for the connecting user" },
336
- "whats_new_seen": { direction: "c2s", handler: "lib/project-sessions.js", description: "Mark a What's New entry as dismissed by the current user" },
337
- "whats_new_seen_result": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Result of marking a What's New entry seen" },
338
335
  "home_dock_get": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Request the current user's home dock preference" },
339
336
  "home_dock_set": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Persist the current user's home dock preference" },
340
337
  "home_dock_state": { direction: "s2c", handler: "lib/public/modules/home-dock.js", description: "Current user home dock preference" },
@@ -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
 
@@ -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 acp.send("session/set_config_option", {
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 acp.send("session/set_mode", { sessionId: state.sessionId, modeId: value }, 15000);
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 acp.send("session/set_mode", { sessionId: state.sessionId, modeId: safeMode }, 15000);
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 acp.send("session/resume", buildSessionParams("resume", {
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 acp.send("session/load", buildSessionParams("load", {
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 acp.send("session/new", buildSessionParams("new", {
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 acp.send("session/prompt", params, 30 * 60 * 1000);
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
- driverRuntime.call(driver, "cancel", driverContext(), function() {
453
- if (state.sessionId && acp.started) acp.notify("session/cancel", { sessionId: state.sessionId });
454
- });
455
- endIterator();
456
- },
457
- close: function() {
458
- messageQueueEnded = true;
459
- if (messageWaiting) {
460
- var resolve = messageWaiting;
461
- messageWaiting = null;
462
- resolve(null);
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
- if (abortController && abortController.signal) {
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