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.
- package/lib/project-session-pair.js +14 -7
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-worker-proposal.js +2 -0
- package/lib/public/modules/worker-proposal.js +1 -0
- package/lib/sdk-bridge.js +4 -0
- package/lib/yoke/adapters/codex.js +33 -0
- package/lib/yoke/codex-background-tasks.js +84 -0
- package/package.json +1 -1
|
@@ -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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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