clay-server 3.4.0-beta.20 → 3.4.0-beta.22

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.
@@ -98,10 +98,15 @@ function attachSessionPair(ctx) {
98
98
  token.delivered = true;
99
99
  var failure = token.failure || null;
100
100
  var response = token.response || "";
101
- var text = "A Worker task delegated through send_to_partner has finished.\n\n" +
102
- "Original task:\n" + token.message + "\n\n" +
103
- (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
104
- "\n\nReview the result, verify it as needed, and continue the task.";
101
+ var text;
102
+ if (token.interrupted) {
103
+ text = "[Worker execution interrupted] The user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
104
+ } else {
105
+ text = "A Worker task delegated through send_to_partner has finished.\n\n" +
106
+ "Original task:\n" + token.message + "\n\n" +
107
+ (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
108
+ "\n\nReview the result, verify it as needed, and continue the task.";
109
+ }
105
110
  sm.sendAndRecord(caller, {
106
111
  type: "user_message",
107
112
  text: text,
@@ -146,6 +151,7 @@ function attachSessionPair(ctx) {
146
151
  }
147
152
  token.response = responseText(partner.history || [], token.startIndex);
148
153
  token.failure = errorSince(partner.history || [], token.startIndex);
154
+ token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
149
155
  finishDelegation(group, caller, partner, token);
150
156
  return resumeDriverWithResult(caller, partner, token);
151
157
  }
@@ -165,8 +171,9 @@ function attachSessionPair(ctx) {
165
171
  clearInterval(timer);
166
172
  finishDelegation(group, caller, partner, token);
167
173
  var failure = errorSince(partner.history || [], token.startIndex);
174
+ var interrupted = !failure && !!partner._lastTurnInterrupted;
168
175
  resolve({
169
- status: failure ? "error" : "complete",
176
+ status: failure ? "error" : (interrupted ? "interrupted" : "complete"),
170
177
  response: responseText(partner.history || [], token.startIndex),
171
178
  error: failure || undefined,
172
179
  });
@@ -260,7 +267,7 @@ function attachSessionPair(ctx) {
260
267
  var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
261
268
  count = Math.max(1, Math.min(5, count));
262
269
  return toolResult({
263
- status: resolved.partner.isProcessing ? "running" : "idle",
270
+ status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
264
271
  partnerId: resolved.partner.localId,
265
272
  title: resolved.partner.title || "New Session",
266
273
  turns: recentTurns(resolved.partner, count),
@@ -383,7 +390,7 @@ function attachSessionPair(ctx) {
383
390
  var group = store.groupForMember(session.localId);
384
391
  var pairPrompt = "";
385
392
  if (group && group.pair && group.pair.driverId === session.localId) {
386
- pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
393
+ pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If a Worker turn is interrupted, its work is partial and unverified; review it and decide next steps with the user. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
387
394
  }
388
395
  return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
389
396
  }
@@ -319,7 +319,7 @@ function attachSessionSpawn(ctx) {
319
319
  statuses.push({
320
320
  localId: session.localId,
321
321
  title: session.title || "New Session",
322
- status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
322
+ status: session.isProcessing ? "running" : (session._lastTurnInterrupted ? "interrupted" : (hasSessionError(session) ? "error" : "done")),
323
323
  turnCount: session.turnCount || 0,
324
324
  lastActivity: session.lastActivity || session.createdAt || 0,
325
325
  });
@@ -248,6 +248,8 @@ function attachWorkerProposal(ctx) {
248
248
  var followup;
249
249
  if (result.status === "complete") {
250
250
  followup = "[Worker execution completed]\nReview and verify the Worker's result. The Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Worker-owned files yourself. If that Worker is no longer available, create a replacement with spawn_sessions for the remaining implementation.\n\n" + (result.response || "The Worker completed without a text summary.");
251
+ } else if (result.status === "interrupted") {
252
+ followup = "[Worker execution interrupted]\nThe user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
251
253
  } else if (result.status === "running") {
252
254
  followup = "[Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
253
255
  } else {
@@ -95,6 +95,7 @@ function statusLabel(status) {
95
95
  if (status === "starting") return "Starting Worker";
96
96
  if (status === "running") return "Worker running";
97
97
  if (status === "completed") return "Completed";
98
+ if (status === "interrupted") return "Interrupted";
98
99
  if (status === "declined") return "Continuing here";
99
100
  if (status === "error") return "Needs attention";
100
101
  return "Suggested by Fable";
package/lib/sdk-bridge.js CHANGED
@@ -885,6 +885,7 @@ function createSDKBridge(opts) {
885
885
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
886
886
  var stillOwnsRuntime = session.queryInstance === myQueryInstance;
887
887
  if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
888
+ session._lastTurnInterrupted = true;
888
889
  session.isProcessing = false;
889
890
  session._awaitingTurnResult = false;
890
891
  session._queuedTurnCount = 0;
@@ -922,6 +923,7 @@ function createSDKBridge(opts) {
922
923
  session._queuedTurnCount = 0;
923
924
  onProcessingChanged();
924
925
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
926
+ session._lastTurnInterrupted = true;
925
927
  if (!session.destroying) {
926
928
  sendAndRecord(session, { type: "thinking_stop" });
927
929
  var interruptMsg2 = (session.vendor && session.vendor !== "claude")
@@ -1189,6 +1191,7 @@ function createSDKBridge(opts) {
1189
1191
  // Wrapper: marks the boot window so pushMessage can buffer instead of
1190
1192
  // dropping messages that arrive while createQuery is still awaiting.
1191
1193
  async function startQuery(session, text, images, linuxUser) {
1194
+ delete session._lastTurnInterrupted;
1192
1195
  session._queryStarting = true;
1193
1196
  try {
1194
1197
  return await startQueryInner(session, text, images, linuxUser);
@@ -1707,6 +1710,7 @@ function createSDKBridge(opts) {
1707
1710
  console.error("[sdk-bridge] QueryHandle rejected message for session " + session.localId + ":", e.message || e);
1708
1711
  }
1709
1712
  if (delivered) {
1713
+ delete session._lastTurnInterrupted;
1710
1714
  if (session._awaitingTurnResult) {
1711
1715
  session._queuedTurnCount = (session._queuedTurnCount || 0) + 1;
1712
1716
  } else {
@@ -8,6 +8,7 @@ var fs = require("fs");
8
8
  var { CodexAppServer } = require("../codex-app-server");
9
9
  var skillDiscovery = require("../skill-discovery");
10
10
  var { resolveOsUserInfo } = require("../../os-users");
11
+ var backgroundTasks = require("../codex-background-tasks");
11
12
 
12
13
  // --- Event flattening ---
13
14
  // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
@@ -672,6 +673,7 @@ function createEventState(model) {
672
673
  toolBlocks: {},
673
674
  commandInputs: {},
674
675
  planTexts: {},
676
+ backgroundTasks: backgroundTasks.createState(),
675
677
  };
676
678
  }
677
679
 
@@ -698,6 +700,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
698
700
  var eventWaiting = null;
699
701
  var iteratorDone = false;
700
702
  var finishedNotified = false;
703
+ var removeBackgroundTaskReset = null;
701
704
 
702
705
  function notifyFinished() {
703
706
  if (finishedNotified) return;
@@ -722,6 +725,12 @@ function createCodexQueryHandle(appServer, queryOpts) {
722
725
  }
723
726
  }
724
727
 
728
+ if (typeof queryOpts.registerBackgroundTaskReset === "function") {
729
+ removeBackgroundTaskReset = queryOpts.registerBackgroundTaskReset(function() {
730
+ backgroundTasks.emitReset(state.backgroundTasks, pushEvent);
731
+ });
732
+ }
733
+
725
734
  function endIterator() {
726
735
  iteratorDone = true;
727
736
  if (eventWaiting) {
@@ -729,6 +738,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
729
738
  eventWaiting = null;
730
739
  resolve({ value: undefined, done: true });
731
740
  }
741
+ if (removeBackgroundTaskReset) {
742
+ removeBackgroundTaskReset();
743
+ removeBackgroundTaskReset = null;
744
+ }
732
745
  notifyFinished();
733
746
  }
734
747
 
@@ -934,6 +947,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
934
947
  pushEvent(yokeEvents[i]);
935
948
  }
936
949
 
950
+ if (method === "turn/completed" || method === "turn/failed") {
951
+ backgroundTasks.poll(appServer, state.threadId, state.backgroundTasks, pushEvent).catch(function() {});
952
+ }
953
+
937
954
  // Resolve turn promise when turn ends
938
955
  if (method === "turn/completed" || method === "turn/failed") {
939
956
  if (turnResolve) {
@@ -1246,6 +1263,20 @@ function createCodexAdapter(opts) {
1246
1263
  ];
1247
1264
  var _cachedModels = CODEX_MODELS.slice();
1248
1265
  var _appServer = null;
1266
+ var _backgroundTaskResetListeners = [];
1267
+
1268
+ function registerBackgroundTaskReset(listener) {
1269
+ _backgroundTaskResetListeners.push(listener);
1270
+ return function() {
1271
+ var index = _backgroundTaskResetListeners.indexOf(listener);
1272
+ if (index !== -1) _backgroundTaskResetListeners.splice(index, 1);
1273
+ };
1274
+ }
1275
+
1276
+ function emitBackgroundTaskReset() {
1277
+ var listeners = _backgroundTaskResetListeners.slice();
1278
+ for (var i = 0; i < listeners.length; i++) listeners[i]();
1279
+ }
1249
1280
  var _initPromise = null;
1250
1281
  var _shutdownPromise = null;
1251
1282
  var _refCount = 0;
@@ -1541,6 +1572,7 @@ function createCodexAdapter(opts) {
1541
1572
  // Spawn and initialize app-server
1542
1573
  _appServer = _createAppServer(serverOpts);
1543
1574
  await _appServer.start();
1575
+ emitBackgroundTaskReset();
1544
1576
 
1545
1577
  await _appServer.send("initialize", {
1546
1578
  clientInfo: { name: "clay", title: "Clay", version: "1.0.0" },
@@ -1676,6 +1708,7 @@ function createCodexAdapter(opts) {
1676
1708
  canUseTool: queryOpts.canUseTool || null,
1677
1709
  onElicitation: queryOpts.onElicitation || null,
1678
1710
  resumeSessionId: queryOpts.resumeSessionId || null,
1711
+ registerBackgroundTaskReset: registerBackgroundTaskReset,
1679
1712
  };
1680
1713
 
1681
1714
  // Reasoning effort
@@ -0,0 +1,84 @@
1
+ function terminalList(result) {
2
+ if (Array.isArray(result)) return result;
3
+ if (!result || typeof result !== "object") return [];
4
+ if (Array.isArray(result.terminals)) return result.terminals;
5
+ if (Array.isArray(result.backgroundTerminals)) return result.backgroundTerminals;
6
+ if (Array.isArray(result.items)) return result.items;
7
+ return [];
8
+ }
9
+
10
+ function isLiveTerminal(terminal) {
11
+ var status = String((terminal && (terminal.status || terminal.state)) || "").toLowerCase();
12
+ return status !== "exited" && status !== "terminated" && status !== "completed" && status !== "failed";
13
+ }
14
+
15
+ function mapTerminals(result) {
16
+ var terminals = terminalList(result);
17
+ var tasks = [];
18
+ for (var i = 0; i < terminals.length; i++) {
19
+ var terminal = terminals[i] || {};
20
+ var taskId = terminal.id || terminal.terminalId || terminal.taskId || "";
21
+ if (!taskId || !isLiveTerminal(terminal)) continue;
22
+ tasks.push({
23
+ task_id: String(taskId),
24
+ task_type: "shell",
25
+ description: terminal.commandLine || terminal.command || terminal.name || "",
26
+ });
27
+ }
28
+ return tasks;
29
+ }
30
+
31
+ function taskIds(tasks) {
32
+ var ids = {};
33
+ for (var i = 0; i < tasks.length; i++) ids[tasks[i].task_id] = true;
34
+ return ids;
35
+ }
36
+
37
+ function sameMembership(first, second) {
38
+ var firstKeys = Object.keys(first || {});
39
+ var secondKeys = Object.keys(second || {});
40
+ if (firstKeys.length !== secondKeys.length) return false;
41
+ for (var i = 0; i < firstKeys.length; i++) {
42
+ if (!second[firstKeys[i]]) return false;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ function createState() {
48
+ return { taskIds: null };
49
+ }
50
+
51
+ function emitIfChanged(state, tasks, pushEvent) {
52
+ var ids = taskIds(tasks);
53
+ if (state.taskIds === null && tasks.length === 0) {
54
+ state.taskIds = ids;
55
+ return false;
56
+ }
57
+ if (state.taskIds && sameMembership(state.taskIds, ids)) return false;
58
+ state.taskIds = ids;
59
+ pushEvent({ yokeType: "background_tasks_changed", tasks: tasks });
60
+ return true;
61
+ }
62
+
63
+ function poll(appServer, threadId, state, pushEvent) {
64
+ if (!appServer || !threadId || appServer._clayBackgroundTasksPollingDisabled) return Promise.resolve(false);
65
+ return appServer.send("thread/backgroundTerminals/list", { threadId: threadId }).then(function(result) {
66
+ return emitIfChanged(state, mapTerminals(result), pushEvent);
67
+ }).catch(function() {
68
+ appServer._clayBackgroundTasksPollingDisabled = true;
69
+ return false;
70
+ });
71
+ }
72
+
73
+ function emitReset(state, pushEvent) {
74
+ state.taskIds = {};
75
+ pushEvent({ yokeType: "background_tasks_changed", tasks: [] });
76
+ }
77
+
78
+ module.exports = {
79
+ createState: createState,
80
+ mapTerminals: mapTerminals,
81
+ emitIfChanged: emitIfChanged,
82
+ poll: poll,
83
+ emitReset: emitReset,
84
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.20",
3
+ "version": "3.4.0-beta.22",
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",