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.
@@ -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 acp.send("session/set_config_option", {
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 acp.send("session/set_model", { sessionId: state.sessionId, modelId: model }, 15000);
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 acp.send("session/load", {
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 acp.send("session/new", {
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 acp.send("session/set_config_option", {
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 acp.send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000).catch(function() {});
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 acp.send("session/prompt", {
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
- if (state.sessionId && acp.started) {
784
- // ACP cancellation is a notification; the in-flight session/prompt will
785
- // resolve with stopReason "cancelled".
786
- acp.notify("session/cancel", { sessionId: state.sessionId });
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
- if (abortController && abortController.signal) {
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 };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.7.0-beta.2",
3
+ "version": "4.7.0-beta.4",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"