@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.
- package/dist/assets/AgentProfilesPage-BFgyFa5e.js +1 -0
- package/dist/assets/ChatPanelHost-Cld9HcVz.js +288 -0
- package/dist/assets/{PluginsPage-B8bHMwfv.js → PluginsPage-DEMqwhOA.js} +1 -1
- package/dist/assets/ScheduledTasksPage-DTP_gedp.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-BOP9CtoI.js → SettingsWorkspacePage-Dr9zT5Bs.js} +112 -112
- package/dist/assets/SharedConversationPage-op3DRwcw.js +1 -0
- package/dist/assets/TerminalDock-BL_UovwU.js +2 -0
- package/dist/assets/WorkspaceInspector-73dhAime.js +13 -0
- package/dist/assets/icons-BP8YOS-Z.js +1 -0
- package/dist/assets/index-Cu2bBHLv.css +3 -0
- package/dist/assets/index-yV1wtqTr.js +66 -0
- package/dist/assets/mcp-servers-dialog-Bp6kbIup.js +5 -0
- package/dist/assets/{monaco-C13M09wD.js → monaco-BTsVCDWS.js} +1 -1
- package/dist/assets/{react-vendor-2RKYr-A4.js → react-vendor-Dr5xvL-e.js} +1 -1
- package/dist/assets/{skills-dialog-MpoxntiV.js → skills-dialog-DiJAUfvW.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/acp/server.mjs +19 -7
- package/server/mcp/registry.mjs +54 -14
- package/server/plugins/loader.mjs +9 -1
- package/server/plugins/registry.mjs +32 -10
- package/server/routes/backup.mjs +79 -72
- package/server/routes/mcp.mjs +0 -5
- package/server/routes/scheduled-tasks.mjs +72 -32
- package/server/routes/workspace.mjs +171 -49
- package/server/utils/scheduled-tasks.mjs +23 -10
- package/server/utils/workspace.mjs +25 -3
- package/dist/assets/AgentProfilesPage-DQ8URegq.js +0 -1
- package/dist/assets/ChatPanelHost-vi1tDqlJ.js +0 -291
- package/dist/assets/ScheduledTasksPage-AaUz5el2.js +0 -2
- package/dist/assets/SharedConversationPage-9A4L9UcQ.js +0 -1
- package/dist/assets/TerminalDock-DQOXpaDe.js +0 -2
- package/dist/assets/WorkspaceInspector-DaM7TJHb.js +0 -13
- package/dist/assets/icons-ko_i0WpN.js +0 -1
- package/dist/assets/index-B2eauKpo.css +0 -3
- package/dist/assets/index-B8Awq5FV.js +0 -63
- package/dist/assets/mcp-servers-dialog-XE4K8PCO.js +0 -5
package/server/routes/backup.mjs
CHANGED
|
@@ -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', '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/server/routes/mcp.mjs
CHANGED
|
@@ -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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
694
|
+
errorMessage: result.error,
|
|
655
695
|
})
|
|
656
696
|
} catch (error) {
|
|
657
697
|
const finishedAt = new Date().toISOString()
|