@openagents-org/agent-launcher 0.2.184 → 0.2.186
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/package.json +1 -1
- package/src/adapters/aider.js +2 -9
- package/src/adapters/amp.js +3 -15
- package/src/adapters/antigravity.js +3 -20
- package/src/adapters/base.js +307 -19
- package/src/adapters/claude.js +63 -47
- package/src/adapters/cline.js +2 -13
- package/src/adapters/codebuddy.js +3 -29
- package/src/adapters/codex.js +3 -13
- package/src/adapters/commandcode.js +3 -29
- package/src/adapters/copilot.js +15 -31
- package/src/adapters/cursor.js +8 -13
- package/src/adapters/deepseek.js +33 -68
- package/src/adapters/gemini.js +3 -20
- package/src/adapters/goose.js +4 -14
- package/src/adapters/hermes.js +2 -10
- package/src/adapters/kimi.js +2 -18
- package/src/adapters/llm-direct.js +17 -6
- package/src/adapters/mini.js +3 -14
- package/src/adapters/nanoclaw.js +25 -13
- package/src/adapters/openclaw.js +41 -0
- package/src/adapters/opencode.js +62 -40
- package/src/adapters/openworker.js +21 -22
- package/src/adapters/pi.js +22 -29
- package/src/daemon.js +54 -4
- package/src/wsl.js +60 -8
package/src/adapters/claude.js
CHANGED
|
@@ -96,12 +96,6 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
96
96
|
this._channelSessions = {}; // channel → Claude CLI session_id
|
|
97
97
|
this._channelProcesses = {}; // channel → child process
|
|
98
98
|
this._stoppingChannels = new Set();
|
|
99
|
-
// Channels that have already announced "Execution stopped by user." for the
|
|
100
|
-
// current stop. Two paths race to post it (the control-action handler that
|
|
101
|
-
// kills the process, and the in-flight message handler that sees
|
|
102
|
-
// pp.userStopped after exit), so this dedups to a single notice. Reset when
|
|
103
|
-
// a new message starts processing in the channel.
|
|
104
|
-
this._stopNoticeSent = new Set();
|
|
105
99
|
this._persistentProcs = {}; // channel → { proc, lineBuffer, pendingLines, idleTimer, messageResolve }
|
|
106
100
|
// Knowledge pinning (decision log + glossary) lives in BaseAdapter; this
|
|
107
101
|
// adapter fetches directly in _handleMessage because the result also
|
|
@@ -139,26 +133,6 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
async _onControlAction(action, payload) {
|
|
142
|
-
if (action === 'stop') {
|
|
143
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
144
|
-
if (channel) {
|
|
145
|
-
const pp = this._persistentProcs[channel];
|
|
146
|
-
if (pp) pp.userStopped = true;
|
|
147
|
-
}
|
|
148
|
-
if (channel && this._channelProcesses[channel]) {
|
|
149
|
-
this._log(`Stopping process for channel=${channel}`);
|
|
150
|
-
this._stoppingChannels.add(channel);
|
|
151
|
-
const proc = this._channelProcesses[channel];
|
|
152
|
-
await this._stopProcess(proc);
|
|
153
|
-
delete this._channelProcesses[channel];
|
|
154
|
-
delete this._channelQueues[channel];
|
|
155
|
-
await this._postStopNotice(channel);
|
|
156
|
-
} else {
|
|
157
|
-
for (const pp of Object.values(this._persistentProcs)) pp.userStopped = true;
|
|
158
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
136
|
if (action === 'restart') {
|
|
163
137
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
164
138
|
if (channel) {
|
|
@@ -317,15 +291,37 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
317
291
|
}
|
|
318
292
|
|
|
319
293
|
/**
|
|
320
|
-
*
|
|
321
|
-
* stop
|
|
322
|
-
*
|
|
323
|
-
*
|
|
294
|
+
* Abandon this turn when a stop landed while it was being prepared. The
|
|
295
|
+
* stop handler kills whatever process is registered, but a turn still
|
|
296
|
+
* working through its pre-CLI round trips has none yet — so without this
|
|
297
|
+
* it goes on to spawn one and the agent resumes seconds after saying it
|
|
298
|
+
* stopped. Returns true when the caller should give up.
|
|
324
299
|
*/
|
|
325
|
-
async
|
|
326
|
-
if (!
|
|
327
|
-
|
|
328
|
-
|
|
300
|
+
async _bailOnStopDuringTurn(msgChannel) {
|
|
301
|
+
if (!this._stoppedBeforeStart(msgChannel)) return false;
|
|
302
|
+
// Deduped: normally the stop handler already announced it.
|
|
303
|
+
await this._finishUserStop(msgChannel);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Stop ONE channel's Claude run (BaseAdapter._handleUserStop drives this).
|
|
309
|
+
*
|
|
310
|
+
* `userStopped` goes on before the kill: the turn waiting on this process
|
|
311
|
+
* must read its exit as a stop, not as a stale session to retry, and the
|
|
312
|
+
* output parser drops what the process wrote before dying.
|
|
313
|
+
*/
|
|
314
|
+
async _stopChannelWork(channel) {
|
|
315
|
+
const pp = this._persistentProcs[channel];
|
|
316
|
+
if (pp) pp.userStopped = true;
|
|
317
|
+
const outcome = await super._stopChannelWork(channel);
|
|
318
|
+
// Those leftover lines are still being parsed on this chain. They post
|
|
319
|
+
// nothing now, but wait them out so none of their bookkeeping races the
|
|
320
|
+
// notice BaseAdapter posts next.
|
|
321
|
+
if (pp) {
|
|
322
|
+
try { await pp.pendingLines; } catch {}
|
|
323
|
+
}
|
|
324
|
+
return outcome;
|
|
329
325
|
}
|
|
330
326
|
|
|
331
327
|
async _stopAllProcesses(completionMessage = 'Execution stopped.') {
|
|
@@ -839,6 +835,23 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
839
835
|
try { event = JSON.parse(line); } catch { return; }
|
|
840
836
|
const eventType = event.type;
|
|
841
837
|
|
|
838
|
+
// The user stopped this process. What it wrote before dying is still
|
|
839
|
+
// queued here, and posting any of it — a thinking block, a tool status,
|
|
840
|
+
// even the finished answer — lands after "Execution stopped by user."
|
|
841
|
+
// and makes the thread read as running again. Keep the session id so
|
|
842
|
+
// the next message resumes the conversation; drop everything else.
|
|
843
|
+
if (pp.userStopped) {
|
|
844
|
+
if (eventType === 'result' && event.session_id) {
|
|
845
|
+
this._channelSessions[pp.msgChannel] = event.session_id;
|
|
846
|
+
this._saveSessions();
|
|
847
|
+
}
|
|
848
|
+
if (eventType === 'result' && pp.messageResolve) {
|
|
849
|
+
pp.messageResolve({ resultEvent: event });
|
|
850
|
+
pp.messageResolve = null;
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
842
855
|
if (eventType === 'assistant') {
|
|
843
856
|
pp.awaitingToolResult = false;
|
|
844
857
|
const blocks = (event.message || {}).content || [];
|
|
@@ -1109,6 +1122,10 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1109
1122
|
/** Queue a reminder about unfinished todos (skipped when this turn IS one). */
|
|
1110
1123
|
async _queueTodoNudge(msgChannel, msg) {
|
|
1111
1124
|
if (msg._todoNudge) return;
|
|
1125
|
+
// A stop ends the plan, it does not pause it. When the stop lands while
|
|
1126
|
+
// this turn is finishing, nudging here is what handed the agent its own
|
|
1127
|
+
// "please continue" right after it announced it had stopped.
|
|
1128
|
+
if (this._stoppingChannels.has(msgChannel)) return;
|
|
1112
1129
|
try {
|
|
1113
1130
|
const remaining = await this.getRemainingTodos(msgChannel);
|
|
1114
1131
|
if (remaining.length > 0) {
|
|
@@ -1136,7 +1153,6 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1136
1153
|
|
|
1137
1154
|
const msgChannel = msg.sessionId || this.channelName;
|
|
1138
1155
|
this._stoppingChannels.delete(msgChannel);
|
|
1139
|
-
this._stopNoticeSent.delete(msgChannel);
|
|
1140
1156
|
const sender = msg.senderName || msg.senderType || 'user';
|
|
1141
1157
|
this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
|
|
1142
1158
|
|
|
@@ -1213,9 +1229,14 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1213
1229
|
await this._killPersistentProc(msgChannel);
|
|
1214
1230
|
} else {
|
|
1215
1231
|
this._log(`Reusing persistent process for ${msgChannel}`);
|
|
1232
|
+
if (await this._bailOnStopDuringTurn(msgChannel)) return;
|
|
1216
1233
|
this._resetIdleTimer(msgChannel);
|
|
1217
1234
|
existingPP.msgChannel = msgChannel;
|
|
1218
1235
|
const result = await this._sendToPersistentProc(existingPP, content);
|
|
1236
|
+
if (existingPP.userStopped) {
|
|
1237
|
+
await this._finishUserStop(msgChannel);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1219
1240
|
if (result.resultEvent) {
|
|
1220
1241
|
const finalResponse = this._composeFinalResponse(existingPP);
|
|
1221
1242
|
if (this._isPromptTooLong(existingPP, finalResponse) && this._channelSessions[msgChannel]) {
|
|
@@ -1228,11 +1249,6 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1228
1249
|
await this._queueTodoNudge(msgChannel, msg);
|
|
1229
1250
|
return;
|
|
1230
1251
|
}
|
|
1231
|
-
} else if (existingPP.userStopped) {
|
|
1232
|
-
if (!existingPP.everPostedAnything) {
|
|
1233
|
-
await this._postStopNotice(msgChannel);
|
|
1234
|
-
}
|
|
1235
|
-
return;
|
|
1236
1252
|
} else {
|
|
1237
1253
|
// Process died mid-message — fall through to spawn a fresh one
|
|
1238
1254
|
this._log(`Persistent process died, falling back to fresh spawn for ${msgChannel}`);
|
|
@@ -1285,6 +1301,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1285
1301
|
}
|
|
1286
1302
|
|
|
1287
1303
|
try {
|
|
1304
|
+
if (await this._bailOnStopDuringTurn(msgChannel)) return;
|
|
1288
1305
|
const pp = this._spawnPersistentProc(msgChannel, cmd, cleanEnv);
|
|
1289
1306
|
// Remember the spawn-time configuration so the fast-path can detect
|
|
1290
1307
|
// staleness on later messages — the pinned knowledge and the mode
|
|
@@ -1301,14 +1318,13 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1301
1318
|
|
|
1302
1319
|
const result = await this._sendToPersistentProc(pp, effectiveContent);
|
|
1303
1320
|
|
|
1321
|
+
if (pp.userStopped) {
|
|
1322
|
+
await this._finishUserStop(msgChannel);
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1304
1326
|
if (result.exited) {
|
|
1305
|
-
this._log(`Process exited during first message (attempt ${attempt + 1})
|
|
1306
|
-
if (pp.userStopped) {
|
|
1307
|
-
if (!pp.everPostedAnything) {
|
|
1308
|
-
await this._postStopNotice(msgChannel);
|
|
1309
|
-
}
|
|
1310
|
-
break;
|
|
1311
|
-
}
|
|
1327
|
+
this._log(`Process exited during first message (attempt ${attempt + 1})`);
|
|
1312
1328
|
if (attempt === 0 && this._channelSessions[msgChannel]) {
|
|
1313
1329
|
this._log(`Stale session detected, retrying without resume`);
|
|
1314
1330
|
delete this._channelSessions[msgChannel];
|
package/src/adapters/cline.js
CHANGED
|
@@ -136,19 +136,6 @@ class ClineAdapter extends BaseAdapter {
|
|
|
136
136
|
// ------------------------------------------------------------------
|
|
137
137
|
|
|
138
138
|
async _onControlAction(action, payload) {
|
|
139
|
-
if (action === 'stop') {
|
|
140
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
141
|
-
if (channel && this._channelProcesses[channel]) {
|
|
142
|
-
this._stoppingChannels.add(channel);
|
|
143
|
-
await this._stopProcess(this._channelProcesses[channel]);
|
|
144
|
-
delete this._channelProcesses[channel];
|
|
145
|
-
delete this._channelQueues[channel];
|
|
146
|
-
try { await this.sendResponse(channel, 'Execution stopped by user.'); } catch {}
|
|
147
|
-
} else {
|
|
148
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
139
|
if (action === 'restart') {
|
|
153
140
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
154
141
|
if (channel) {
|
|
@@ -631,6 +618,8 @@ class ClineAdapter extends BaseAdapter {
|
|
|
631
618
|
* `ok` reflects run_result.finishReason === "completed".
|
|
632
619
|
*/
|
|
633
620
|
_runCline(channel, clineBin, args, workingDir) {
|
|
621
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
622
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve({ userStopped: true });
|
|
634
623
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
635
624
|
const [cmd, ...spawnArgs] = this._spawnableCmd(clineBin, args);
|
|
636
625
|
|
|
@@ -76,7 +76,6 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
76
76
|
|
|
77
77
|
// Max wall-clock for a single headless run, after which the process group is
|
|
78
78
|
// killed and the turn reported as interrupted.
|
|
79
|
-
const TIMEOUT_MS = 600000; // 10 minutes
|
|
80
79
|
|
|
81
80
|
// Idle watchdog: with no stdout frame for this long we nudge the channel, and
|
|
82
81
|
// after MAX consecutive silences we kill a run that is probably wedged.
|
|
@@ -176,29 +175,9 @@ class CodeBuddyAdapter extends BaseAdapter {
|
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
// ------------------------------------------------------------------
|
|
179
|
-
//
|
|
178
|
+
// Shutdown
|
|
180
179
|
// ------------------------------------------------------------------
|
|
181
180
|
|
|
182
|
-
async _onControlAction(action, payload) {
|
|
183
|
-
if (action === 'stop') {
|
|
184
|
-
const channel = (payload && payload.channel) || null;
|
|
185
|
-
if (channel) {
|
|
186
|
-
const proc = this._channelProcesses[channel];
|
|
187
|
-
if (proc) {
|
|
188
|
-
this._stoppingChannels.add(channel);
|
|
189
|
-
await this._stopProcess(proc);
|
|
190
|
-
delete this._channelProcesses[channel];
|
|
191
|
-
delete this._channelQueues[channel];
|
|
192
|
-
try { await this.sendResponse(channel, 'Execution stopped.'); } catch {}
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
await this._stopAllProcesses();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
return super._onControlAction(action, payload);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
181
|
stop() {
|
|
203
182
|
super.stop();
|
|
204
183
|
void this._stopAllProcesses('Agent stopped.');
|
|
@@ -591,6 +570,8 @@ class CodeBuddyAdapter extends BaseAdapter {
|
|
|
591
570
|
* @returns {Promise<object>} { code, signal, result, sessionId, anyOutput, lastAssistantText, userStopped, stderr }
|
|
592
571
|
*/
|
|
593
572
|
_runCodeBuddy(channel, bin, args, workingDir, prompt) {
|
|
573
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
574
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve({ userStopped: true });
|
|
594
575
|
return new Promise((resolve) => {
|
|
595
576
|
const [cmd, ...spawnArgs] = this._spawnableCmd(bin, args);
|
|
596
577
|
this._log(`Spawning: ${path.basename(cmd)} ${redactArgs(spawnArgs).join(' ')} (cwd=${workingDir})`);
|
|
@@ -633,7 +614,6 @@ class CodeBuddyAdapter extends BaseAdapter {
|
|
|
633
614
|
if (settled) return;
|
|
634
615
|
settled = true;
|
|
635
616
|
clearInterval(watchdog);
|
|
636
|
-
clearTimeout(timeout);
|
|
637
617
|
if (this._channelProcesses[channel] === proc) delete this._channelProcesses[channel];
|
|
638
618
|
resolve(payload);
|
|
639
619
|
};
|
|
@@ -739,12 +719,6 @@ class CodeBuddyAdapter extends BaseAdapter {
|
|
|
739
719
|
}
|
|
740
720
|
}, WATCHDOG_INTERVAL_MS);
|
|
741
721
|
|
|
742
|
-
const timeout = setTimeout(() => {
|
|
743
|
-
this._log(`Run exceeded ${TIMEOUT_MS}ms — killing`);
|
|
744
|
-
killedByWatchdog = true;
|
|
745
|
-
void this._stopProcess(proc);
|
|
746
|
-
}, TIMEOUT_MS);
|
|
747
|
-
|
|
748
722
|
try {
|
|
749
723
|
proc.stdin.on('error', () => {});
|
|
750
724
|
proc.stdin.write(prompt, 'utf-8');
|
package/src/adapters/codex.js
CHANGED
|
@@ -247,19 +247,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
247
247
|
} catch {}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
async _onControlAction(action, payload) {
|
|
251
|
-
if (action === 'stop') {
|
|
252
|
-
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
253
|
-
await this._stopProcess(proc);
|
|
254
|
-
delete this._channelProcesses[channel];
|
|
255
|
-
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
// Shared actions (status, routines, skill.install, skill.uninstall).
|
|
260
|
-
await super._onControlAction(action, payload);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
250
|
// ------------------------------------------------------------------
|
|
264
251
|
// Message handler
|
|
265
252
|
// ------------------------------------------------------------------
|
|
@@ -325,6 +312,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
325
312
|
|
|
326
313
|
try {
|
|
327
314
|
const result = await this._spawnCodex(cmd, env, msgChannel, fullPrompt);
|
|
315
|
+
if (result.stopped) return;
|
|
328
316
|
|
|
329
317
|
if (result.responseText) {
|
|
330
318
|
await this.sendResponse(msgChannel, result.responseText);
|
|
@@ -351,6 +339,8 @@ class CodexAdapter extends BaseAdapter {
|
|
|
351
339
|
}
|
|
352
340
|
|
|
353
341
|
async _spawnCodex(cmd, env, msgChannel, prompt) {
|
|
342
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
343
|
+
if (this._stoppedBeforeStart(msgChannel)) return { stopped: true, responseText: '', exitCode: null };
|
|
354
344
|
return new Promise((resolve, reject) => {
|
|
355
345
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
356
346
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -71,7 +71,6 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
71
71
|
|
|
72
72
|
// Max wall-clock for a single headless run, after which the process group is
|
|
73
73
|
// killed and the turn reported as interrupted.
|
|
74
|
-
const TIMEOUT_MS = 600000; // 10 minutes
|
|
75
74
|
|
|
76
75
|
// Idle watchdog: with no stdout frame for this long we nudge the channel, and
|
|
77
76
|
// after MAX consecutive silences we kill a run that is probably wedged.
|
|
@@ -175,29 +174,9 @@ class CommandCodeAdapter extends BaseAdapter {
|
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
// ------------------------------------------------------------------
|
|
178
|
-
//
|
|
177
|
+
// Shutdown
|
|
179
178
|
// ------------------------------------------------------------------
|
|
180
179
|
|
|
181
|
-
async _onControlAction(action, payload) {
|
|
182
|
-
if (action === 'stop') {
|
|
183
|
-
const channel = (payload && payload.channel) || null;
|
|
184
|
-
if (channel) {
|
|
185
|
-
const proc = this._channelProcesses[channel];
|
|
186
|
-
if (proc) {
|
|
187
|
-
this._stoppingChannels.add(channel);
|
|
188
|
-
await this._stopProcess(proc);
|
|
189
|
-
delete this._channelProcesses[channel];
|
|
190
|
-
delete this._channelQueues[channel];
|
|
191
|
-
try { await this.sendResponse(channel, 'Execution stopped.'); } catch {}
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
await this._stopAllProcesses();
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
return super._onControlAction(action, payload);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
180
|
stop() {
|
|
202
181
|
super.stop();
|
|
203
182
|
void this._stopAllProcesses('Agent stopped.');
|
|
@@ -545,6 +524,8 @@ class CommandCodeAdapter extends BaseAdapter {
|
|
|
545
524
|
* @returns {Promise<object>} { code, signal, result, anyOutput, userStopped, stderrSessionId }
|
|
546
525
|
*/
|
|
547
526
|
_runCommandCode(channel, bin, args, workingDir, prompt) {
|
|
527
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
528
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve({ userStopped: true });
|
|
548
529
|
return new Promise((resolve) => {
|
|
549
530
|
const [cmd, ...spawnArgs] = this._spawnableCmd(bin, args);
|
|
550
531
|
this._log(`Spawning: ${path.basename(cmd)} ${redactArgs(spawnArgs).join(' ')} (cwd=${workingDir})`);
|
|
@@ -585,7 +566,6 @@ class CommandCodeAdapter extends BaseAdapter {
|
|
|
585
566
|
if (settled) return;
|
|
586
567
|
settled = true;
|
|
587
568
|
clearInterval(watchdog);
|
|
588
|
-
clearTimeout(timeout);
|
|
589
569
|
if (this._channelProcesses[channel] === proc) delete this._channelProcesses[channel];
|
|
590
570
|
resolve(payload);
|
|
591
571
|
};
|
|
@@ -671,12 +651,6 @@ class CommandCodeAdapter extends BaseAdapter {
|
|
|
671
651
|
}
|
|
672
652
|
}, WATCHDOG_INTERVAL_MS);
|
|
673
653
|
|
|
674
|
-
const timeout = setTimeout(() => {
|
|
675
|
-
this._log(`Run exceeded ${TIMEOUT_MS}ms — killing`);
|
|
676
|
-
killedByWatchdog = true;
|
|
677
|
-
void this._stopProcess(proc);
|
|
678
|
-
}, TIMEOUT_MS);
|
|
679
|
-
|
|
680
654
|
// Feed the prompt. stdin is closed immediately: Command Code times out a
|
|
681
655
|
// piped stdin after 30s of silence, so an unclosed pipe would stall.
|
|
682
656
|
try {
|
package/src/adapters/copilot.js
CHANGED
|
@@ -76,7 +76,7 @@ const SECRET_ENV_VARS = [
|
|
|
76
76
|
const ACT_ALLOW_TOOLS = ['shell', 'write'];
|
|
77
77
|
|
|
78
78
|
// How long to wait for a turn before giving up (no output / hung CLI).
|
|
79
|
-
const
|
|
79
|
+
const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
80
80
|
|
|
81
81
|
class CopilotAdapter extends BaseAdapter {
|
|
82
82
|
/**
|
|
@@ -336,32 +336,6 @@ class CopilotAdapter extends BaseAdapter {
|
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
// ------------------------------------------------------------------
|
|
340
|
-
// Control actions (stop / interrupt)
|
|
341
|
-
// ------------------------------------------------------------------
|
|
342
|
-
|
|
343
|
-
async _onControlAction(action, payload) {
|
|
344
|
-
if (action === 'stop') {
|
|
345
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
346
|
-
if (channel && this._channelProcesses[channel]) {
|
|
347
|
-
this._stoppingChannels.add(channel);
|
|
348
|
-
await this._stopProcess(this._channelProcesses[channel]);
|
|
349
|
-
delete this._channelProcesses[channel];
|
|
350
|
-
delete this._channelQueues[channel];
|
|
351
|
-
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
352
|
-
} else {
|
|
353
|
-
for (const [ch, proc] of Object.entries(this._channelProcesses)) {
|
|
354
|
-
this._stoppingChannels.add(ch);
|
|
355
|
-
await this._stopProcess(proc);
|
|
356
|
-
delete this._channelProcesses[ch];
|
|
357
|
-
try { await this.sendStatus(ch, 'Execution stopped by user'); } catch {}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
await super._onControlAction(action, payload);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
339
|
/**
|
|
366
340
|
* Daemon shutdown: tear down any in-flight Copilot subprocess so the thread
|
|
367
341
|
* doesn't get stuck showing "running".
|
|
@@ -544,6 +518,8 @@ class CopilotAdapter extends BaseAdapter {
|
|
|
544
518
|
* `errorMessage` / `staleSession` / `timedOut` so the caller can react.
|
|
545
519
|
*/
|
|
546
520
|
_runTurn(channel, args) {
|
|
521
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
522
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve({ userStopped: true });
|
|
547
523
|
const [file, ...prefix] = this._resolveExec(this._copilotBin);
|
|
548
524
|
const env = getEnhancedEnv({ ...(this.agentEnv || process.env) });
|
|
549
525
|
const cwd = this.workingDir || defaultAgentWorkdir(this.agentName);
|
|
@@ -589,10 +565,15 @@ class CopilotAdapter extends BaseAdapter {
|
|
|
589
565
|
let pending = Promise.resolve();
|
|
590
566
|
let settled = false;
|
|
591
567
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
568
|
+
let timeout = null;
|
|
569
|
+
const resetTimeout = () => {
|
|
570
|
+
if (timeout) clearTimeout(timeout);
|
|
571
|
+
timeout = setTimeout(() => {
|
|
572
|
+
this._stopProcess(proc).catch(() => {});
|
|
573
|
+
finish({ timedOut: true, finalText: accumulatedText() });
|
|
574
|
+
}, TURN_IDLE_TIMEOUT_MS);
|
|
575
|
+
};
|
|
576
|
+
resetTimeout();
|
|
596
577
|
|
|
597
578
|
const finish = (summary) => {
|
|
598
579
|
if (settled) return;
|
|
@@ -681,6 +662,9 @@ class CopilotAdapter extends BaseAdapter {
|
|
|
681
662
|
}
|
|
682
663
|
};
|
|
683
664
|
|
|
665
|
+
if (proc.stdout) proc.stdout.on('data', resetTimeout);
|
|
666
|
+
if (proc.stderr) proc.stderr.on('data', resetTimeout);
|
|
667
|
+
|
|
684
668
|
proc.stdout.on('data', (chunk) => {
|
|
685
669
|
for (const ev of parser.push(chunk)) {
|
|
686
670
|
pending = pending.then(() => handle(ev)).catch(() => {});
|
package/src/adapters/cursor.js
CHANGED
|
@@ -62,18 +62,7 @@ class CursorAdapter extends BaseAdapter {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async _onControlAction(action, payload) {
|
|
65
|
-
if (action === '
|
|
66
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
67
|
-
if (channel && this._channelProcesses[channel]) {
|
|
68
|
-
this._stoppingChannels.add(channel);
|
|
69
|
-
await this._stopProcess(this._channelProcesses[channel]);
|
|
70
|
-
delete this._channelProcesses[channel];
|
|
71
|
-
delete this._channelQueues[channel];
|
|
72
|
-
try { await this.sendResponse(channel, 'Execution stopped.'); } catch {}
|
|
73
|
-
} else {
|
|
74
|
-
await this._stopAllProcesses('Execution stopped.');
|
|
75
|
-
}
|
|
76
|
-
} else if (action === 'restart') {
|
|
65
|
+
if (action === 'restart') {
|
|
77
66
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
78
67
|
if (channel) {
|
|
79
68
|
if (this._channelProcesses[channel]) {
|
|
@@ -86,7 +75,10 @@ class CursorAdapter extends BaseAdapter {
|
|
|
86
75
|
this._saveSessions();
|
|
87
76
|
try { await this.sendResponse(channel, 'Session cleared. Send a new message to start fresh.'); } catch {}
|
|
88
77
|
}
|
|
78
|
+
return;
|
|
89
79
|
}
|
|
80
|
+
// Shared actions (status, routines, skill.install, model.set).
|
|
81
|
+
await super._onControlAction(action, payload);
|
|
90
82
|
}
|
|
91
83
|
|
|
92
84
|
stop() {
|
|
@@ -444,6 +436,9 @@ class CursorAdapter extends BaseAdapter {
|
|
|
444
436
|
return;
|
|
445
437
|
}
|
|
446
438
|
|
|
439
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
440
|
+
if (this._stoppedBeforeStart(msgChannel)) return;
|
|
441
|
+
|
|
447
442
|
try {
|
|
448
443
|
const resolved = this._resolveToNodeCmd(cmd[0]);
|
|
449
444
|
if (resolved) {
|
|
@@ -573,7 +568,7 @@ class CursorAdapter extends BaseAdapter {
|
|
|
573
568
|
const stoppedByUser = this._stoppingChannels.has(msgChannel);
|
|
574
569
|
|
|
575
570
|
if (stoppedByUser) {
|
|
576
|
-
|
|
571
|
+
// BaseAdapter._handleUserStop cancels the plan before announcing.
|
|
577
572
|
} else if (!msg._todoNudge) {
|
|
578
573
|
try {
|
|
579
574
|
const remaining = await this.getRemainingTodos(msgChannel);
|
package/src/adapters/deepseek.js
CHANGED
|
@@ -123,18 +123,8 @@ const RECAP_TAIL = 60;
|
|
|
123
123
|
const TOKEN_ENV = 'OPENAGENTS_WORKSPACE_TOKEN';
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* The
|
|
127
|
-
*
|
|
128
|
-
* The client clears its "stopping" state only on a status matching
|
|
129
|
-
* /stopped|stopping failed/i (chat-view.tsx). Any other final status leaves the
|
|
130
|
-
* session marked as working forever — and a cancelled run produces nothing
|
|
131
|
-
* later that could correct it. The wording is therefore load-bearing, and the
|
|
132
|
-
* tests assert it against the client's own regex.
|
|
133
|
-
*/
|
|
134
|
-
const STOPPED_STATUS = 'Execution stopped by user';
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The status for a stop that could NOT be confirmed.
|
|
126
|
+
* The notice for a stop that could NOT be confirmed (BaseAdapter posts it when
|
|
127
|
+
* `_stopChannelWork` reports 'failed').
|
|
138
128
|
*
|
|
139
129
|
* Contains "stopping failed" because that is the other half of the client's
|
|
140
130
|
* terminal-status regex. Reporting a clean stop when the child may still be
|
|
@@ -169,6 +159,7 @@ class DeepSeekAdapter extends BaseAdapter {
|
|
|
169
159
|
// arriving in that window has no process to kill. Without this set the stop
|
|
170
160
|
// would be silently dropped and the task would start anyway.
|
|
171
161
|
this._busyChannels = new Set();
|
|
162
|
+
this._stopFailedMessage = STOP_FAILED_STATUS;
|
|
172
163
|
// Channels cancelled while busy. Checked at every await boundary and once
|
|
173
164
|
// more immediately before spawning.
|
|
174
165
|
this._cancelledChannels = new Set();
|
|
@@ -590,78 +581,52 @@ class DeepSeekAdapter extends BaseAdapter {
|
|
|
590
581
|
}
|
|
591
582
|
|
|
592
583
|
// ------------------------------------------------------------------
|
|
593
|
-
//
|
|
584
|
+
// Stop (BaseAdapter._handleUserStop drives these)
|
|
594
585
|
// ------------------------------------------------------------------
|
|
595
586
|
|
|
596
|
-
async _onControlAction(action, payload) {
|
|
597
|
-
if (action === 'stop') {
|
|
598
|
-
// The workspace sends the conversation it wants stopped. Honouring it is
|
|
599
|
-
// what keeps one user's stop from tearing down every other concurrent
|
|
600
|
-
// run — claude.js already scopes its stop this way; mini/amp do not, and
|
|
601
|
-
// following them here was the mistake. A stop with no channel still
|
|
602
|
-
// means "everything".
|
|
603
|
-
const target = (payload && typeof payload === 'object' && payload.channel)
|
|
604
|
-
? payload.channel
|
|
605
|
-
: null;
|
|
606
|
-
await this._stopChannels(target);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
await super._onControlAction(action, payload);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
587
|
/**
|
|
613
|
-
* Stop
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* a clean one, because the child may still be writing to the workspace.
|
|
588
|
+
* Stop ONE conversation. A run that has not spawned yet is cancelled by
|
|
589
|
+
* flagging it — nothing is running, so that is complete by definition.
|
|
590
|
+
* A child whose kill could not be confirmed reports 'failed', and the user
|
|
591
|
+
* is told the truth: it may still be writing to the workspace.
|
|
618
592
|
*/
|
|
619
|
-
async
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const busy = target
|
|
624
|
-
? (this._busyChannels.has(target) ? [target] : [])
|
|
625
|
-
: [...this._busyChannels];
|
|
626
|
-
for (const channel of busy) {
|
|
593
|
+
async _stopChannelWork(channel) {
|
|
594
|
+
let outcome = 'idle';
|
|
595
|
+
if (this._busyChannels.has(channel)) {
|
|
627
596
|
this._cancelledChannels.add(channel);
|
|
628
|
-
this._stoppingChannels.add(channel);
|
|
629
597
|
// Release a waiter parked on the shared bootstrap. Marking alone is not
|
|
630
598
|
// enough: the flag is only read between awaits, and this one can last
|
|
631
599
|
// the whole bootstrap timeout.
|
|
632
600
|
const release = this._cancelWaiters.get(channel);
|
|
633
601
|
if (release) release();
|
|
634
|
-
|
|
635
|
-
outcome.set(channel, true);
|
|
602
|
+
outcome = 'stopped';
|
|
636
603
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (channel === BOOTSTRAP_KEY) {
|
|
640
|
-
// Bootstrap is SHARED: every channel waits on the same profile
|
|
641
|
-
// compose. Tearing it down because one conversation was stopped would
|
|
642
|
-
// fail the others too, so a scoped stop leaves it alone.
|
|
643
|
-
if (target) continue;
|
|
644
|
-
} else if (target && channel !== target) {
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
|
|
604
|
+
const proc = this._channelProcesses[channel];
|
|
605
|
+
if (proc) {
|
|
648
606
|
// Untrack only once the kill has been attempted, so the child is never
|
|
649
607
|
// missing from the registry while it may still be running.
|
|
650
608
|
const ended = await this._stopProcess(proc);
|
|
651
609
|
delete this._channelProcesses[channel];
|
|
652
|
-
|
|
653
|
-
if (channel === BOOTSTRAP_KEY) continue;
|
|
654
|
-
this._stoppingChannels.add(channel);
|
|
655
|
-
outcome.set(channel, ended);
|
|
610
|
+
outcome = ended ? 'stopped' : 'failed';
|
|
656
611
|
}
|
|
612
|
+
return outcome;
|
|
613
|
+
}
|
|
657
614
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
615
|
+
_channelsWithWork() {
|
|
616
|
+
return [...new Set([...super._channelsWithWork(), ...this._busyChannels])]
|
|
617
|
+
.filter((channel) => channel !== BOOTSTRAP_KEY);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Bootstrap is SHARED: every conversation waits on the same profile
|
|
622
|
+
* compose. A scoped stop leaves it alone, or it would fail the others too;
|
|
623
|
+
* only a workspace-wide stop tears it down.
|
|
624
|
+
*/
|
|
625
|
+
async _stopSharedWork() {
|
|
626
|
+
const proc = this._channelProcesses[BOOTSTRAP_KEY];
|
|
627
|
+
if (!proc) return;
|
|
628
|
+
await this._stopProcess(proc);
|
|
629
|
+
delete this._channelProcesses[BOOTSTRAP_KEY];
|
|
665
630
|
}
|
|
666
631
|
|
|
667
632
|
/** True when a child has ended, by exit code OR by signal. */
|
|
@@ -987,7 +952,7 @@ class DeepSeekAdapter extends BaseAdapter {
|
|
|
987
952
|
try {
|
|
988
953
|
// Last gate before anything is started. Everything above this line is
|
|
989
954
|
// reversible; a process is not.
|
|
990
|
-
if (this._cancelled(msgChannel)) return { text: '', error: null };
|
|
955
|
+
if (this._cancelled(msgChannel) || this._stoppedBeforeStart(msgChannel)) return { text: '', error: null };
|
|
991
956
|
return await this._spawnDsh({ taskFile, msgChannel, permissionMode });
|
|
992
957
|
} finally {
|
|
993
958
|
try { fs.rmSync(taskFile, { force: true }); } catch { /* best effort */ }
|