@shawnstack/quickforge 1.7.0 → 1.7.2

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.
Files changed (37) hide show
  1. package/dist/assets/AgentProfilesPage-BFgyFa5e.js +1 -0
  2. package/dist/assets/ChatPanelHost-Cld9HcVz.js +288 -0
  3. package/dist/assets/{PluginsPage-B8bHMwfv.js → PluginsPage-DEMqwhOA.js} +1 -1
  4. package/dist/assets/ScheduledTasksPage-DTP_gedp.js +2 -0
  5. package/dist/assets/{SettingsWorkspacePage-BOP9CtoI.js → SettingsWorkspacePage-Dr9zT5Bs.js} +112 -112
  6. package/dist/assets/SharedConversationPage-op3DRwcw.js +1 -0
  7. package/dist/assets/TerminalDock-BL_UovwU.js +2 -0
  8. package/dist/assets/WorkspaceInspector-73dhAime.js +13 -0
  9. package/dist/assets/icons-BP8YOS-Z.js +1 -0
  10. package/dist/assets/index-Cu2bBHLv.css +3 -0
  11. package/dist/assets/index-yV1wtqTr.js +66 -0
  12. package/dist/assets/mcp-servers-dialog-Bp6kbIup.js +5 -0
  13. package/dist/assets/{monaco-C13M09wD.js → monaco-BTsVCDWS.js} +1 -1
  14. package/dist/assets/{react-vendor-2RKYr-A4.js → react-vendor-Dr5xvL-e.js} +1 -1
  15. package/dist/assets/{skills-dialog-MpoxntiV.js → skills-dialog-DiJAUfvW.js} +1 -1
  16. package/dist/index.html +6 -6
  17. package/package.json +1 -1
  18. package/server/acp/server.mjs +19 -7
  19. package/server/mcp/registry.mjs +54 -14
  20. package/server/plugins/loader.mjs +9 -1
  21. package/server/plugins/registry.mjs +32 -10
  22. package/server/routes/backup.mjs +79 -72
  23. package/server/routes/mcp.mjs +0 -5
  24. package/server/routes/scheduled-tasks.mjs +72 -32
  25. package/server/routes/workspace.mjs +171 -49
  26. package/server/utils/scheduled-tasks.mjs +23 -10
  27. package/server/utils/workspace.mjs +25 -3
  28. package/dist/assets/AgentProfilesPage-DQ8URegq.js +0 -1
  29. package/dist/assets/ChatPanelHost-vi1tDqlJ.js +0 -291
  30. package/dist/assets/ScheduledTasksPage-AaUz5el2.js +0 -2
  31. package/dist/assets/SharedConversationPage-9A4L9UcQ.js +0 -1
  32. package/dist/assets/TerminalDock-DQOXpaDe.js +0 -2
  33. package/dist/assets/WorkspaceInspector-DaM7TJHb.js +0 -13
  34. package/dist/assets/icons-ko_i0WpN.js +0 -1
  35. package/dist/assets/index-B2eauKpo.css +0 -3
  36. package/dist/assets/index-B8Awq5FV.js +0 -63
  37. package/dist/assets/mcp-servers-dialog-XE4K8PCO.js +0 -5
@@ -7,18 +7,14 @@ import {
7
7
  readStore,
8
8
  writeStore,
9
9
  readProjectConfigData,
10
- writeProjectConfigData,
11
10
  storageDir,
12
11
  } from '../storage.mjs'
13
- import { initializeActiveProject } from '../project-config.mjs'
14
- import { setActiveWorkspaceRootForFilesystem } from './filesystem.mjs'
15
- import { getWorkspaceRoot } from '../utils/workspace.mjs'
16
12
 
17
13
  const BACKUP_VERSION = 1
18
14
  const BACKUP_APP = 'quickforge'
19
15
  const IMPORT_UPLOAD_MAX_BYTES = Number(process.env.QUICKFORGE_IMPORT_UPLOAD_MAX_BYTES || 1024 * 1024 * 1024)
20
16
  const backupScopes = new Set(['all', 'config', 'sessions'])
21
- const settingsSectionIds = ['settings', 'mcp', 'providerKeys', 'customProviders', 'projects', 'scheduledTasks']
17
+ const settingsSectionIds = ['settings', 'mcp', 'providerKeys', 'customProviders', 'scheduledTasks']
22
18
  const exportSectionIds = new Set(settingsSectionIds)
23
19
  const restoreSectionIds = new Set([...settingsSectionIds, 'conversations'])
24
20
  const restoreModes = new Set(['replace', 'merge'])
@@ -138,21 +134,6 @@ function assertObjectSection(value, name) {
138
134
  return value
139
135
  }
140
136
 
141
- function assertProjectConfig(value) {
142
- const projectConfig = assertObjectSection(value, 'projects')
143
- if (projectConfig === undefined) return undefined
144
- if (!Array.isArray(projectConfig.projects)) {
145
- const error = new Error('Invalid backup section: projects.projects must be an array')
146
- error.statusCode = 400
147
- throw error
148
- }
149
- return {
150
- activeProjectId: typeof projectConfig.activeProjectId === 'string' ? projectConfig.activeProjectId : null,
151
- globalSkills: Array.isArray(projectConfig.globalSkills) ? projectConfig.globalSkills : [],
152
- projects: projectConfig.projects,
153
- }
154
- }
155
-
156
137
  function filterSessionsByMetadata(sessions, metadata) {
157
138
  if (!sessions || !metadata) return sessions
158
139
  const metadataIds = new Set(Object.keys(metadata))
@@ -226,8 +207,8 @@ async function buildBackup(scope = 'all', options = {}) {
226
207
  shouldInclude('mcp') ? readStore('mcp').then((value) => ['mcp', value]) : null,
227
208
  shouldInclude('providerKeys') ? readStore('provider-keys').then((value) => ['providerKeys', value]) : null,
228
209
  shouldInclude('customProviders') ? readStore('custom-providers').then((value) => ['customProviders', value]) : null,
229
- shouldInclude('projects') ? readProjectConfigData().then((value) => ['projects', value]) : null,
230
210
  shouldInclude('scheduledTasks') ? readStore('scheduled-tasks').then((value) => ['scheduledTasks', value]) : null,
211
+ options.includeLocalProjects ? readProjectConfigData().then((value) => ['projects', value]) : null,
231
212
  ])
232
213
  for (const entry of entries) {
233
214
  if (entry) data[entry[0]] = entry[1]
@@ -264,7 +245,14 @@ function normalizeBackupPayload(payload) {
264
245
  throw error
265
246
  }
266
247
 
267
- const data = backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)
248
+ const hasEnvelope = backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)
249
+ if (hasEnvelope && Number.isInteger(backup.version) && backup.version > BACKUP_VERSION) {
250
+ const error = new Error(`Unsupported backup version: ${backup.version}`)
251
+ error.statusCode = 400
252
+ throw error
253
+ }
254
+
255
+ const data = hasEnvelope
268
256
  ? backup.data
269
257
  : backup
270
258
 
@@ -310,14 +298,13 @@ function validateSettingsImportBackup(payload) {
310
298
  const normalized = normalizeBackupPayload(payload)
311
299
  const validSections = {}
312
300
  const invalidSections = {}
301
+ const ignoredProjects = normalized.sections.projects !== undefined
313
302
 
314
303
  for (const id of settingsSectionIds) {
315
304
  const value = normalized.sections[id]
316
305
  if (value === undefined) continue
317
306
  try {
318
- validSections[id] = id === 'projects'
319
- ? assertProjectConfig(value)
320
- : assertObjectSection(value, id)
307
+ validSections[id] = assertObjectSection(value, id)
321
308
  } catch (error) {
322
309
  invalidSections[id] = error instanceof Error ? error.message : `Invalid backup section: ${id}`
323
310
  }
@@ -339,6 +326,7 @@ function validateSettingsImportBackup(payload) {
339
326
  data: validSections,
340
327
  },
341
328
  invalidSections,
329
+ ignoredProjects,
342
330
  }
343
331
  }
344
332
 
@@ -351,18 +339,25 @@ function validateBackupPayload(payload) {
351
339
  ? normalizeSessionMetadata(sessions, sections.sessionsMetadata)
352
340
  : assertObjectSection(sections.sessionsMetadata, 'sessionsMetadata')
353
341
 
342
+ const validatedSections = {
343
+ settings: assertObjectSection(sections.settings, 'settings'),
344
+ mcp: assertObjectSection(sections.mcp, 'mcp'),
345
+ providerKeys: assertObjectSection(sections.providerKeys, 'providerKeys'),
346
+ customProviders: assertObjectSection(sections.customProviders, 'customProviders'),
347
+ scheduledTasks: assertObjectSection(sections.scheduledTasks, 'scheduledTasks'),
348
+ sessions,
349
+ sessionsMetadata,
350
+ }
351
+
352
+ if (Object.values(validatedSections).every((value) => value === undefined)) {
353
+ const error = new Error('Backup does not contain any restorable sections')
354
+ error.statusCode = 400
355
+ throw error
356
+ }
357
+
354
358
  return {
355
359
  ...backup,
356
- sections: {
357
- settings: assertObjectSection(sections.settings, 'settings'),
358
- mcp: assertObjectSection(sections.mcp, 'mcp'),
359
- providerKeys: assertObjectSection(sections.providerKeys, 'providerKeys'),
360
- customProviders: assertObjectSection(sections.customProviders, 'customProviders'),
361
- projects: assertProjectConfig(sections.projects),
362
- scheduledTasks: assertObjectSection(sections.scheduledTasks, 'scheduledTasks'),
363
- sessions,
364
- sessionsMetadata,
365
- },
360
+ sections: validatedSections,
366
361
  }
367
362
  }
368
363
 
@@ -411,7 +406,6 @@ function filterRestoreSections(sections, selected) {
411
406
  mcp: selected.has('mcp') ? sections.mcp : undefined,
412
407
  providerKeys: selected.has('providerKeys') ? sections.providerKeys : undefined,
413
408
  customProviders: selected.has('customProviders') ? sections.customProviders : undefined,
414
- projects: selected.has('projects') ? sections.projects : undefined,
415
409
  scheduledTasks: selected.has('scheduledTasks') ? sections.scheduledTasks : undefined,
416
410
  sessions: selected.has('conversations') ? sections.sessions : undefined,
417
411
  sessionsMetadata: selected.has('conversations') ? sections.sessionsMetadata : undefined,
@@ -444,29 +438,67 @@ function countKeys(value) {
444
438
  return value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).length : 0
445
439
  }
446
440
 
441
+ async function localProjectIds() {
442
+ const config = await readProjectConfigData()
443
+ return new Set(
444
+ (Array.isArray(config?.projects) ? config.projects : [])
445
+ .map((project) => project?.id)
446
+ .filter((id) => typeof id === 'string'),
447
+ )
448
+ }
449
+
450
+ function scheduledTasksWithMissingProjects(tasks, projectIds) {
451
+ if (!tasks || typeof tasks !== 'object' || Array.isArray(tasks)) return []
452
+ return Object.values(tasks).filter((task) => (
453
+ task && typeof task === 'object' && !Array.isArray(task) &&
454
+ typeof task.projectId === 'string' && task.projectId && !projectIds.has(task.projectId)
455
+ ))
456
+ }
457
+
458
+ function pauseScheduledTasksWithMissingProjects(tasks, projectIds) {
459
+ if (!tasks || typeof tasks !== 'object' || Array.isArray(tasks)) return tasks
460
+ return Object.fromEntries(Object.entries(tasks).map(([id, task]) => {
461
+ if (
462
+ task && typeof task === 'object' && !Array.isArray(task) &&
463
+ typeof task.projectId === 'string' && task.projectId && !projectIds.has(task.projectId)
464
+ ) {
465
+ return [id, { ...task, status: 'paused' }]
466
+ }
467
+ return [id, task]
468
+ }))
469
+ }
470
+
447
471
  function buildSummary(sections) {
448
472
  const summary = {}
449
473
  if (sections.settings !== undefined) summary.settings = countKeys(sections.settings)
450
474
  if (sections.mcp !== undefined) summary.mcp = Array.isArray(sections.mcp?.mcpServers) ? sections.mcp.mcpServers.length : countKeys(sections.mcp)
451
475
  if (sections.providerKeys !== undefined) summary.providerKeys = countKeys(sections.providerKeys)
452
476
  if (sections.customProviders !== undefined) summary.customProviders = countKeys(sections.customProviders)
453
- if (sections.projects !== undefined) summary.projects = sections.projects.projects.length
454
477
  if (sections.scheduledTasks !== undefined) summary.scheduledTasks = countKeys(sections.scheduledTasks)
455
478
  if (sections.sessions !== undefined) summary.sessions = countKeys(filterSessionsByMetadata(sections.sessions, sections.sessionsMetadata))
456
479
  if (sections.sessionsMetadata !== undefined) summary.sessionsMetadata = countKeys(sections.sessionsMetadata)
457
480
  return summary
458
481
  }
459
482
 
460
- function inspectBackup(payload) {
483
+ async function inspectBackup(payload) {
484
+ const normalized = normalizeBackupPayload(payload)
485
+ const ignoredProjects = normalized.sections.projects !== undefined
461
486
  const backup = validateBackupPayload(payload)
462
487
  const summary = buildSummary(backup.sections)
463
488
  const warnings = []
464
489
  const containsSecrets = countKeys(backup.sections.providerKeys) > 0
490
+ const missingProjectTasks = backup.sections.scheduledTasks === undefined
491
+ ? []
492
+ : scheduledTasksWithMissingProjects(backup.sections.scheduledTasks, await localProjectIds())
465
493
 
466
494
  if (containsSecrets) warnings.push('Backup contains API keys.')
495
+ if (ignoredProjects) warnings.push('Project lists and project-specific settings are local machine data and will not be imported.')
467
496
  if (backup.sections.sessions !== undefined || backup.sections.sessionsMetadata !== undefined) {
468
497
  warnings.push('Importing conversations will replace local conversation data.')
469
498
  }
499
+ if (missingProjectTasks.length > 0) {
500
+ warnings.push(`${missingProjectTasks.length} scheduled task(s) reference projects that are not registered locally and will be paused after import.`)
501
+ }
470
502
 
471
503
  return {
472
504
  ok: true,
@@ -481,7 +513,7 @@ function inspectBackup(payload) {
481
513
  }
482
514
 
483
515
  async function writeSafetyBackup(scope = 'config') {
484
- const backup = await buildBackup(scope, { includeSecrets: true })
516
+ const backup = await buildBackup(scope, { includeSecrets: true, includeLocalProjects: true })
485
517
  const dir = path.join(storageDir, 'backups')
486
518
  await fs.mkdir(dir, { recursive: true })
487
519
  const file = path.join(dir, `quickforge-before-restore-${backupTimestamp()}.json`)
@@ -495,26 +527,6 @@ function mergeRecordStore(localValue, backupValue) {
495
527
  return { ...(localValue && typeof localValue === 'object' ? localValue : {}), ...backupValue }
496
528
  }
497
529
 
498
- // Merge projects config: dedupe the projects array by id (backup wins on
499
- // collision, local-only entries preserved), take activeProjectId / globalSkills
500
- // from backup.
501
- function mergeProjectConfig(localConfig, backupConfig) {
502
- const localProjects = Array.isArray(localConfig?.projects) ? localConfig.projects : []
503
- const backupProjects = Array.isArray(backupConfig.projects) ? backupConfig.projects : []
504
- const merged = new Map()
505
- for (const project of localProjects) {
506
- if (project && typeof project.id === 'string') merged.set(project.id, project)
507
- }
508
- for (const project of backupProjects) {
509
- if (project && typeof project.id === 'string') merged.set(project.id, project)
510
- }
511
- return {
512
- activeProjectId: typeof backupConfig.activeProjectId === 'string' ? backupConfig.activeProjectId : (localConfig?.activeProjectId ?? null),
513
- globalSkills: Array.isArray(backupConfig.globalSkills) ? backupConfig.globalSkills : (Array.isArray(localConfig?.globalSkills) ? localConfig.globalSkills : []),
514
- projects: [...merged.values()],
515
- }
516
- }
517
-
518
530
  async function restoreValidatedBackup(backup, mode = 'replace') {
519
531
  const merge = mode === 'merge'
520
532
  const { sections } = backup
@@ -544,16 +556,10 @@ async function restoreValidatedBackup(backup, mode = 'replace') {
544
556
  summary.customProviders = countKeys(value)
545
557
  }
546
558
 
547
- if (sections.projects !== undefined) {
548
- const value = merge ? mergeProjectConfig(await readProjectConfigData(), sections.projects) : sections.projects
549
- await writeProjectConfigData(value)
550
- await initializeActiveProject()
551
- setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
552
- summary.projects = value.projects.length
553
- }
554
-
555
559
  if (sections.scheduledTasks !== undefined) {
556
- const value = merge ? mergeRecordStore(await readStore('scheduled-tasks'), sections.scheduledTasks) : sections.scheduledTasks
560
+ const projectIds = await localProjectIds()
561
+ const safeTasks = pauseScheduledTasksWithMissingProjects(sections.scheduledTasks, projectIds)
562
+ const value = merge ? mergeRecordStore(await readStore('scheduled-tasks'), safeTasks) : safeTasks
557
563
  await writeStore('scheduled-tasks', value)
558
564
  summary.scheduledTasks = countKeys(value)
559
565
  }
@@ -587,7 +593,7 @@ export async function handleBackupApi(req, res, url) {
587
593
  if (req.method === 'POST' && url.pathname === '/api/backup/inspect') {
588
594
  await ensureStorage()
589
595
  const body = await readJsonBody(req)
590
- sendJson(res, 200, inspectBackup(body))
596
+ sendJson(res, 200, await inspectBackup(body))
591
597
  return
592
598
  }
593
599
 
@@ -595,10 +601,11 @@ export async function handleBackupApi(req, res, url) {
595
601
  await ensureStorage()
596
602
  const text = await readTextBody(req)
597
603
  const { backup, ignoredConversations } = extractSettingsBackupFromText(text)
598
- const { backup: validBackup, invalidSections } = validateSettingsImportBackup(backup)
599
- const inspect = inspectBackup(validBackup)
604
+ const { backup: validBackup, invalidSections, ignoredProjects } = validateSettingsImportBackup(backup)
605
+ const inspect = await inspectBackup(validBackup)
606
+ if (ignoredProjects) inspect.warnings.push('Project lists and project-specific settings are local machine data and will not be imported.')
600
607
  const token = await writePendingImportBackup(validBackup)
601
- sendJson(res, 200, { ...inspect, invalidSections, ignoredConversations, importToken: token })
608
+ sendJson(res, 200, { ...inspect, invalidSections, ignoredConversations, ignoredProjects, importToken: token })
602
609
  return
603
610
  }
604
611
 
@@ -49,11 +49,6 @@ export async function handleMcpApi(req, res, url) {
49
49
  return
50
50
  }
51
51
 
52
- if (req.method === 'POST' && url.pathname === '/api/mcp/reconnect') {
53
- sendJson(res, 200, await refreshMcpAndAgentTools())
54
- return
55
- }
56
-
57
52
  if (req.method === 'POST' && parts[0] === 'api' && parts[1] === 'mcp' && parts[2] === 'reconnect' && parts[3]) {
58
53
  await reconnectMcpServer(decodeURIComponent(parts[3]))
59
54
  sendJson(res, 200, await refreshMcpAndAgentTools())
@@ -1,7 +1,7 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai/compat'
2
2
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
3
3
  import { readStore, atomicUpdate } from '../storage.mjs'
4
- import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
4
+ import { createAgent, getSessionEventBus, agentEvents, persistSessionState, abortRun } from '../agent-manager.mjs'
5
5
  import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
6
6
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
7
7
  import { logger } from '../utils/logger.mjs'
@@ -436,21 +436,30 @@ async function updateTask(taskId, updater) {
436
436
  return updated
437
437
  }
438
438
 
439
+ function recurringTaskStatusRepair(task, now) {
440
+ if (task?.status !== 'completed' || !isRecurringTask(task)) return null
441
+ const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > now.getTime()
442
+ ? task.nextRunAt
443
+ : calculateNextRun(task, now)
444
+ if (!nextRunAt) return null
445
+ return {
446
+ ...task,
447
+ status: 'enabled',
448
+ nextRunAt,
449
+ scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
450
+ updatedAt: now.toISOString(),
451
+ }
452
+ }
453
+
439
454
  async function repairRecurringTaskStatuses() {
455
+ const now = new Date()
456
+ const snapshot = await readStore(STORE)
457
+ if (!Object.values(snapshot).some((task) => recurringTaskStatusRepair(task, now))) return
458
+
440
459
  await atomicUpdate(STORE, (data) => {
441
460
  for (const [taskId, task] of Object.entries(data)) {
442
- if (task?.status !== 'completed' || !isRecurringTask(task)) continue
443
- const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > Date.now()
444
- ? task.nextRunAt
445
- : calculateNextRun(task)
446
- if (!nextRunAt) continue
447
- data[taskId] = {
448
- ...task,
449
- status: 'enabled',
450
- nextRunAt,
451
- scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
452
- updatedAt: new Date().toISOString(),
453
- }
461
+ const repaired = recurringTaskStatusRepair(task, now)
462
+ if (repaired) data[taskId] = repaired
454
463
  }
455
464
  return data
456
465
  })
@@ -566,14 +575,15 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
566
575
  onStarted?.({ taskId: task.id, runId, sessionId })
567
576
 
568
577
  const eventBus = getSessionEventBus(sessionId)
578
+ const runtimeLimitMs = Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000))
579
+ let timeout = null
580
+ let handler = null
581
+ let timedOut = false
582
+ let resolveFinished
569
583
  const finished = new Promise((resolve) => {
570
- const cleanup = (handler, timeout) => {
571
- clearTimeout(timeout)
572
- eventBus?.removeListener('agent_event', handler)
573
- }
574
- const handler = (event) => {
584
+ resolveFinished = resolve
585
+ handler = (event) => {
575
586
  if (event.type !== 'agent_end') return
576
- cleanup(handler, timeout)
577
587
  const errorMessage = event.errorMessage || session.agent.state.errorMessage
578
588
  const aborted = session.status === 'aborted' || session.agent.state.messages.some((message) => message?.role === 'assistant' && message?.stopReason === 'aborted')
579
589
  resolve({
@@ -583,24 +593,54 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
583
593
  messages: event.messages ?? session.agent.state.messages,
584
594
  })
585
595
  }
586
- const timeout = setTimeout(() => {
587
- cleanup(handler, timeout)
588
- resolve({ ok: false, aborted: false, error: '执行超时', messages: session.agent.state.messages })
589
- }, Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000)))
590
596
  eventBus?.on('agent_event', handler)
591
597
  })
592
598
 
599
+ const runPromise = (async () => {
600
+ try {
601
+ await session.agent.continue()
602
+ } catch (continueError) {
603
+ if (continueError?.message !== 'Request was aborted' && continueError?.message !== 'Scheduled task aborted') {
604
+ throw continueError
605
+ }
606
+ }
607
+ return finished
608
+ })()
609
+ const timeoutPromise = new Promise((resolve) => {
610
+ timeout = setTimeout(() => resolve({ timedOut: true }), runtimeLimitMs)
611
+ })
612
+
613
+ let result
593
614
  try {
594
- await session.agent.continue()
595
- } catch (continueError) {
596
- if (continueError?.message === 'Request was aborted' || continueError?.message === 'Scheduled task aborted') {
597
- settled = true
615
+ const outcome = await Promise.race([
616
+ runPromise.then((value) => ({ result: value })),
617
+ timeoutPromise,
618
+ ])
619
+ if (outcome.timedOut) {
620
+ timedOut = true
621
+ const abortPromise = Promise.resolve()
622
+ .then(() => abortRun(sessionId))
623
+ .catch((error) => {
624
+ logger.warn(`Failed to abort timed out scheduled task ${task.id}:`, error)
625
+ })
626
+ await Promise.race([
627
+ abortPromise,
628
+ new Promise((resolve) => setTimeout(resolve, 1000)),
629
+ ])
630
+ await Promise.race([
631
+ runPromise.catch(() => undefined),
632
+ new Promise((resolve) => setTimeout(resolve, 1000)),
633
+ ])
634
+ result = { ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages }
598
635
  } else {
599
- throw continueError
636
+ result = outcome.result
600
637
  }
638
+ } finally {
639
+ clearTimeout(timeout)
640
+ eventBus?.removeListener('agent_event', handler)
641
+ if (timedOut) resolveFinished?.({ ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages })
601
642
  }
602
- const result = await finished
603
- if (result.aborted) settled = true
643
+ settled = true
604
644
  const finishedAt = new Date().toISOString()
605
645
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
606
646
  const aiResult = result.ok ? latestAssistantText(result.messages) : ''
@@ -634,7 +674,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
634
674
  inputContent: run.inputContent ?? latestTask.instruction,
635
675
  aiResult: result.ok ? aiResult : undefined,
636
676
  result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
637
- errorMessage: result.aborted ? '已暂停执行' : result.error,
677
+ errorMessage: result.error,
638
678
  sessionId,
639
679
  agentId: executionAgent?.id || latestTask.agentId || null,
640
680
  agentLabel: executionAgent?.label || null,
@@ -651,7 +691,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
651
691
  sessionId,
652
692
  status: result.aborted ? 'failed' : (result.ok ? 'success' : 'failed'),
653
693
  result: aiResult,
654
- errorMessage: result.aborted ? '已暂停执行' : result.error,
694
+ errorMessage: result.error,
655
695
  })
656
696
  } catch (error) {
657
697
  const finishedAt = new Date().toISOString()