@yeaft/webchat-agent 1.0.12 → 1.0.13

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.
@@ -38,7 +38,7 @@ import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
39
39
  import { discoverLlmModels } from '../llm-model-discovery.js';
40
40
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
41
- import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
41
+ import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
42
42
  import { startYeaftStatusRefresh, refreshYeaftStatus } from '../yeaft/status-cache.js';
43
43
 
44
44
  export async function handleMessage(msg) {
@@ -658,6 +658,9 @@ export async function handleMessage(msg) {
658
658
  case 'yeaft_sub_agent_prompt':
659
659
  handleYeaftSubAgentPrompt(msg);
660
660
  break;
661
+ case 'yeaft_task_cancel':
662
+ handleYeaftTaskCancel(msg);
663
+ break;
661
664
 
662
665
  // wave-6b: manual dream trigger from VP detail page
663
666
  case 'yeaft_dream_trigger':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,6 +12,7 @@ import { getRuntimePlatformInfo } from '../runtime-platform.js';
12
12
 
13
13
  const LOG_PREVIEW_BYTES = 4096;
14
14
  const SUB_AGENT_LOG_PREVIEW_BYTES = 1024 * 1024;
15
+ const DEFAULT_CANCEL_ESCALATION_MS = 2000;
15
16
 
16
17
  function logPreviewBytesFor(task) {
17
18
  return task?.kind === 'sub_agent' ? SUB_AGENT_LOG_PREVIEW_BYTES : LOG_PREVIEW_BYTES;
@@ -45,14 +46,23 @@ function publicSnapshot(task) {
45
46
  };
46
47
  }
47
48
 
49
+ function taskCommand(task) {
50
+ const command = task?.runtime?.command;
51
+ return typeof command === 'string' && command.trim() ? command.trim() : '';
52
+ }
53
+
48
54
  export class TaskManager {
49
- constructor({ yeaftDir, onEvent = null, runtimePlatform = null } = {}) {
55
+ constructor({ yeaftDir, onEvent = null, runtimePlatform = null, cancelEscalationMs = DEFAULT_CANCEL_ESCALATION_MS } = {}) {
50
56
  if (!yeaftDir) throw new Error('TaskManager requires yeaftDir');
51
57
  this.store = new TaskStore({ yeaftDir });
52
58
  this.onEvent = typeof onEvent === 'function' ? onEvent : null;
53
59
  this.runtimePlatform = runtimePlatform || getRuntimePlatformInfo();
60
+ this.cancelEscalationMs = Number.isFinite(cancelEscalationMs)
61
+ ? Math.max(0, Math.floor(cancelEscalationMs))
62
+ : DEFAULT_CANCEL_ESCALATION_MS;
54
63
  this.active = new Map();
55
64
  this.processes = new Map();
65
+ this.cancelEscalationTimers = new Map();
56
66
  this.#loadPersistedRunningTasks();
57
67
  }
58
68
 
@@ -191,9 +201,15 @@ export class TaskManager {
191
201
  const key = this.#key(sessionId, taskId);
192
202
  const task = this.active.get(key) || this.store.readTask(sessionId, taskId);
193
203
  if (!task || isTerminalTaskStatus(task.status)) return publicSnapshot(task);
204
+ const escalationTimer = this.cancelEscalationTimers.get(key);
205
+ if (escalationTimer) {
206
+ clearTimeout(escalationTimer);
207
+ this.cancelEscalationTimers.delete(key);
208
+ }
194
209
  const logPath = task.log?.path || this.store.logPath(sessionId, taskId);
195
210
  const tail = this.store.readLogFile(logPath, { tail: true, maxBytes: logPreviewBytesFor(task) });
196
- task.status = status || TASK_STATUS.FAILED;
211
+ const cancelRequested = !!task.runtime?.cancelRequestedAt;
212
+ task.status = cancelRequested ? TASK_STATUS.CANCELLED : (status || TASK_STATUS.FAILED);
197
213
  task.updatedAt = nowIso();
198
214
  task.endedAt = nowIso();
199
215
  task.log = { ...(task.log || {}), path: tail.path, bytes: tail.bytes, preview: tail.text };
@@ -212,19 +228,61 @@ export class TaskManager {
212
228
  if (!task) return { ok: false, error: `Unknown task: ${taskId}` };
213
229
  if (isTerminalTaskStatus(task.status)) return { ok: true, task: publicSnapshot(task) };
214
230
  const runner = this.processes.get(key);
215
- const killed = runner ? runner.kill('SIGTERM') : false;
216
- if (!killed) {
231
+ if (!runner) {
217
232
  return {
218
233
  ok: false,
219
- error: 'Unable to cancel task: no live process handle or process-tree kill failed.',
234
+ error: 'Unable to cancel task: no live process handle.',
220
235
  task: publicSnapshot(task),
221
236
  };
222
237
  }
223
- const completed = this.#completeTask(sessionId, taskId, {
224
- status: TASK_STATUS.CANCELLED,
225
- signal: 'SIGTERM',
226
- });
227
- return { ok: true, task: completed };
238
+
239
+ if (!task.runtime?.cancelRequestedAt) {
240
+ const signalled = runner.kill('SIGTERM');
241
+ if (!signalled) {
242
+ return {
243
+ ok: false,
244
+ error: 'Unable to cancel task: process-tree signal failed.',
245
+ task: publicSnapshot(task),
246
+ };
247
+ }
248
+ const cancelRequestedAt = nowIso();
249
+ task.runtime = {
250
+ ...(task.runtime || {}),
251
+ cancelRequestedAt,
252
+ cancelSignal: 'SIGTERM',
253
+ cancelEscalationMs: this.cancelEscalationMs,
254
+ };
255
+ task.updatedAt = cancelRequestedAt;
256
+ this.store.writeTask(task);
257
+ this.active.set(key, task);
258
+ this.store.appendEvent(sessionId, { event: 'cancel_requested', taskId, signal: 'SIGTERM' });
259
+ this.#emit('updated', task, { cancelRequested: true });
260
+
261
+ if (this.cancelEscalationMs >= 0 && !this.cancelEscalationTimers.has(key)) {
262
+ const timer = setTimeout(() => {
263
+ this.cancelEscalationTimers.delete(key);
264
+ const current = this.active.get(key) || this.store.readTask(sessionId, taskId);
265
+ if (!current || isTerminalTaskStatus(current.status)) return;
266
+ const liveRunner = this.processes.get(key);
267
+ const escalated = liveRunner ? liveRunner.kill('SIGKILL') : false;
268
+ current.runtime = {
269
+ ...(current.runtime || {}),
270
+ cancelEscalatedAt: nowIso(),
271
+ cancelEscalatedSignal: 'SIGKILL',
272
+ cancelEscalationFailed: !escalated,
273
+ };
274
+ current.updatedAt = current.runtime.cancelEscalatedAt;
275
+ this.store.writeTask(current);
276
+ this.active.set(key, current);
277
+ this.store.appendEvent(sessionId, { event: 'cancel_escalated', taskId, signal: 'SIGKILL', ok: escalated });
278
+ this.#emit('updated', current, { cancelEscalated: true, cancelEscalationOk: escalated });
279
+ }, this.cancelEscalationMs);
280
+ if (typeof timer.unref === 'function') timer.unref();
281
+ this.cancelEscalationTimers.set(key, timer);
282
+ }
283
+ }
284
+
285
+ return { ok: true, task: publicSnapshot(task), pending: true };
228
286
  }
229
287
 
230
288
  listActiveTasks(sessionId = null) {
@@ -274,7 +332,9 @@ export class TaskManager {
274
332
  const lines = ['<active_tasks>'];
275
333
  for (const task of tasks) {
276
334
  const preview = (task.log?.preview || '').trim().split('\n').slice(-3).join(' | ');
277
- lines.push(`- ${task.id} | ${task.kind} | ${task.status} | owner=${task.ownerVpId || 'unknown'} | title=${JSON.stringify(task.title)} | log=${task.log?.path || ''}${preview ? ` | tail=${JSON.stringify(preview)}` : ''}`);
335
+ const command = taskCommand(task);
336
+ const cancelRequestedAt = typeof task.runtime?.cancelRequestedAt === 'string' ? task.runtime.cancelRequestedAt : '';
337
+ lines.push(`- ${task.id} | ${task.kind} | ${task.status} | owner=${task.ownerVpId || 'unknown'} | title=${JSON.stringify(task.title)}${command ? ` | command=${JSON.stringify(command)}` : ''}${cancelRequestedAt ? ` | cancelRequestedAt=${JSON.stringify(cancelRequestedAt)}` : ''} | log=${task.log?.path || ''}${preview ? ` | tail=${JSON.stringify(preview)}` : ''}`);
278
338
  }
279
339
  lines.push('</active_tasks>');
280
340
  return lines.join('\n');
@@ -1217,6 +1217,7 @@ function formatTaskResultForVp(task) {
1217
1217
  `<task-result id="${task.id}" kind="${task.kind}" status="${task.status}">`,
1218
1218
  `title: ${task.title || task.kind || task.id}`,
1219
1219
  ];
1220
+ if (task?.runtime?.command) lines.push(`command: ${task.runtime.command}`);
1220
1221
  if (result.exitCode !== undefined && result.exitCode !== null) lines.push(`exitCode: ${result.exitCode}`);
1221
1222
  if (result.signal) lines.push(`signal: ${result.signal}`);
1222
1223
  if (result.error) lines.push(`error: ${result.error}`);
@@ -4489,6 +4490,54 @@ export function handleYeaftSubAgentPrompt(msg) {
4489
4490
  }, { sessionId, vpId: task.ownerVpId || null, threadId: task.source?.threadId || null });
4490
4491
  }
4491
4492
 
4493
+ export function handleYeaftTaskCancel(msg) {
4494
+ const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
4495
+ const taskId = typeof msg?.taskId === 'string' ? msg.taskId.trim() : '';
4496
+ const clientRequestId = typeof msg?.clientRequestId === 'string' ? msg.clientRequestId.trim() : '';
4497
+ const fail = (error, task = null) => {
4498
+ sendSessionEvent({
4499
+ type: 'yeaft_task_cancel_result',
4500
+ success: false,
4501
+ taskId: taskId || null,
4502
+ clientRequestId: clientRequestId || null,
4503
+ error,
4504
+ ...(task ? { task } : {}),
4505
+ }, sessionId ? { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null } : undefined);
4506
+ };
4507
+
4508
+ if (!sessionId || !taskId) {
4509
+ fail('sessionId and taskId are required');
4510
+ return;
4511
+ }
4512
+ if (!session?.taskManager || typeof session.taskManager.cancelTask !== 'function') {
4513
+ fail('task manager unavailable');
4514
+ return;
4515
+ }
4516
+
4517
+ let result;
4518
+ try {
4519
+ result = session.taskManager.cancelTask(sessionId, taskId);
4520
+ } catch (err) {
4521
+ fail(err?.message || String(err));
4522
+ return;
4523
+ }
4524
+
4525
+ const task = result?.task || session.taskManager.getTask?.(sessionId, taskId) || null;
4526
+ if (!result?.ok) {
4527
+ fail(result?.error || 'Failed to cancel task', task);
4528
+ return;
4529
+ }
4530
+
4531
+ sendSessionEvent({
4532
+ type: 'yeaft_task_cancel_result',
4533
+ success: true,
4534
+ taskId,
4535
+ clientRequestId: clientRequestId || null,
4536
+ pending: !!result?.pending,
4537
+ task,
4538
+ }, { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null });
4539
+ }
4540
+
4492
4541
  /** Deprecated mode switch — Yeaft is single-mode. */
4493
4542
  export function handleYeaftModeSwitch(_msg) {
4494
4543
  console.warn('[Yeaft] yeaft_mode_switch is deprecated and ignored — Yeaft now runs in a single unified mode.');