@shawnstack/quickforge 1.7.7 → 1.7.8
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-BSM5w3bn.js +1 -0
- package/dist/assets/ChatPanelHost-cd80S4EX.js +48 -0
- package/dist/assets/CloudAccountSettingsPage-BJMrfgy9.js +1 -0
- package/dist/assets/{PluginsPage-CPE5tbbf.js → PluginsPage-mJkTmVVV.js} +1 -1
- package/dist/assets/ScheduledTasksPage-DJj_NLm9.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-BE2T4d3P.js → SettingsWorkspacePage-7lQ0Wl63.js} +389 -389
- package/dist/assets/ShareLinksSettingsPage-DjFq_Lnb.js +1 -0
- package/dist/assets/SharedConversationPage-BbuNpcpP.js +1 -0
- package/dist/assets/TerminalDock-CczcCJJE.js +2 -0
- package/dist/assets/WorkspaceInspector-0i6x-diB.js +36 -0
- package/dist/assets/{icons-2D18YbSI.js → icons-DM_qJ7VJ.js} +1 -1
- package/dist/assets/{index-BiCTgUlY.css → index-D-J_8Smf.css} +1 -1
- package/dist/assets/index-DFASc15l.js +66 -0
- package/dist/assets/local-tools-Dn1Y9aPe.js +270 -0
- package/dist/assets/{model-display-label-C6-0eDxF.js → logger-8REuLPGK.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-GY7sdfIr.js → mcp-servers-dialog-BG4b67ea.js} +2 -2
- package/dist/assets/{model-reference-o41fJ6jQ.js → model-display-label-CURwH0xM.js} +1 -1
- package/dist/assets/{monaco-DvXErxp2.js → monaco-bDr860xx.js} +1 -1
- package/dist/assets/{react-vendor-3UaA-C9W.js → react-vendor-BpWW_Nvp.js} +1 -1
- package/dist/assets/{skills-dialog-C1fJ1Jl7.js → skills-dialog-DIzZvQqW.js} +3 -3
- package/dist/index.html +8 -8
- package/package.json +1 -1
- package/server/acp/server.mjs +17 -1
- package/server/agent-manager.mjs +251 -31
- package/server/ai-http-logger.mjs +162 -19
- package/server/ai-provider-options.mjs +4 -1
- package/server/auto-archive.mjs +42 -29
- package/server/cloud/auto-approval.mjs +155 -0
- package/server/cloud/client.mjs +5 -0
- package/server/cloud/qf-agent-process.mjs +163 -3
- package/server/cloud/service-config.mjs +1 -1
- package/server/index.mjs +69 -13
- package/server/lan-access-backup.mjs +256 -0
- package/server/lan-access-cutover.mjs +306 -0
- package/server/lan-access-json-file.mjs +75 -0
- package/server/lan-access-service.mjs +155 -0
- package/server/lan-access-store.mjs +110 -9
- package/server/maintenance/downgrade-lan-access-v1.mjs +101 -0
- package/server/maintenance/downgrade-session-state-v1.mjs +130 -0
- package/server/maintenance/downgrade-share-v1.mjs +101 -0
- package/server/maintenance/export-lan-access-v1.mjs +80 -0
- package/server/maintenance/export-scheduled-runs-v1.mjs +45 -0
- package/server/maintenance/export-session-state-v1.mjs +67 -0
- package/server/maintenance/export-share-v1.mjs +77 -0
- package/server/routes/agent.mjs +37 -4
- package/server/routes/backup.mjs +221 -39
- package/server/routes/cloud.mjs +28 -1
- package/server/routes/scheduled-tasks.mjs +347 -134
- package/server/routes/shares.mjs +4 -2
- package/server/routes/storage.mjs +138 -8
- package/server/routes/workspace.mjs +265 -4
- package/server/scheduled-runs-backup.mjs +235 -0
- package/server/scheduled-runs-cutover.mjs +497 -0
- package/server/scheduled-task-runs-service.mjs +279 -0
- package/server/session-index-service.mjs +495 -0
- package/server/session-state-backup.mjs +277 -0
- package/server/session-state-cutover.mjs +370 -0
- package/server/session-state-service.mjs +709 -0
- package/server/share-backup.mjs +255 -0
- package/server/share-cutover.mjs +296 -0
- package/server/share-json-file.mjs +60 -0
- package/server/share-service.mjs +163 -0
- package/server/share-store.mjs +196 -19
- package/server/skills.mjs +0 -1
- package/server/sqlite/database.mjs +233 -0
- package/server/sqlite/lan-access-repository.mjs +652 -0
- package/server/sqlite/migrations.mjs +577 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +464 -0
- package/server/sqlite/session-index-repository.mjs +297 -0
- package/server/sqlite/session-state-repository.mjs +786 -0
- package/server/sqlite/share-repository.mjs +854 -0
- package/server/storage.mjs +317 -1
- package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +0 -1
- package/dist/assets/ChatPanelHost-Dul1ahKj.js +0 -307
- package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +0 -1
- package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +0 -2
- package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +0 -1
- package/dist/assets/SharedConversationPage-BK9XJnEe.js +0 -1
- package/dist/assets/TerminalDock-CL7HU9DI.js +0 -2
- package/dist/assets/WorkspaceInspector-MNBLIKMn.js +0 -36
- package/dist/assets/index-BTPzyq7d.js +0 -65
- package/dist/assets/mermaid-renderer-jEbqFaTe.js +0 -2
- /package/dist/assets/{plugin-api-Uu2xAB4d.js → plugin-api-BrdQeM4B.js} +0 -0
|
@@ -7,6 +7,14 @@ import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
|
|
|
7
7
|
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
8
8
|
import { logger } from '../utils/logger.mjs'
|
|
9
9
|
import { resolveModelBinding } from '../model-catalog.mjs'
|
|
10
|
+
import { createScheduledTaskRunsService } from '../scheduled-task-runs-service.mjs'
|
|
11
|
+
import {
|
|
12
|
+
assertScheduledRunsAvailable,
|
|
13
|
+
canStartScheduledRun,
|
|
14
|
+
configureScheduledRunsRuntimeHooks,
|
|
15
|
+
isScheduledRunsAuthoritative,
|
|
16
|
+
recordScheduledRunsDiagnostic,
|
|
17
|
+
} from '../scheduled-runs-cutover.mjs'
|
|
10
18
|
import {
|
|
11
19
|
dayMs,
|
|
12
20
|
formatLocalDateTime,
|
|
@@ -29,7 +37,9 @@ const editableScheduleTypes = new Set(['once', 'daily', 'weekly', 'monthly'])
|
|
|
29
37
|
const weekDayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
30
38
|
|
|
31
39
|
let schedulerTimer = null
|
|
40
|
+
let runnerDesired = false
|
|
32
41
|
let running = false
|
|
42
|
+
let tickPromise = null
|
|
33
43
|
const runningTaskRunIds = new Map()
|
|
34
44
|
|
|
35
45
|
function executionModeFor(task) {
|
|
@@ -385,61 +395,94 @@ function getTasks() {
|
|
|
385
395
|
return readStore(STORE).then((data) => Object.values(data).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))))
|
|
386
396
|
}
|
|
387
397
|
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return Math.min(parsed, max)
|
|
398
|
+
async function hydrateTaskRuns(task, limit = 5) {
|
|
399
|
+
if (!task || !isScheduledRunsAuthoritative()) return task
|
|
400
|
+
return { ...task, runs: await taskRunsService.recentRuns(task.id, limit) }
|
|
392
401
|
}
|
|
393
402
|
|
|
394
|
-
function
|
|
395
|
-
if (!
|
|
396
|
-
|
|
397
|
-
.filter(Boolean)
|
|
398
|
-
.join('\n')
|
|
399
|
-
.toLowerCase()
|
|
400
|
-
return text.includes(keyword.toLowerCase())
|
|
403
|
+
async function hydrateTasksRuns(tasks, limit = 5) {
|
|
404
|
+
if (!isScheduledRunsAuthoritative()) return tasks
|
|
405
|
+
return Promise.all(tasks.map((task) => hydrateTaskRuns(task, limit)))
|
|
401
406
|
}
|
|
402
407
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const taskId = String(params.get('taskId') || '').trim()
|
|
408
|
-
const status = String(params.get('status') || '').trim()
|
|
409
|
-
const trigger = String(params.get('trigger') || '').trim()
|
|
410
|
-
const keyword = String(params.get('keyword') || '').trim()
|
|
411
|
-
const startedFrom = params.get('startedFrom') ? new Date(String(params.get('startedFrom'))).getTime() : null
|
|
412
|
-
const startedTo = params.get('startedTo') ? new Date(String(params.get('startedTo'))).getTime() : null
|
|
413
|
-
|
|
414
|
-
let runs = (await getTasks()).flatMap((task) => (task.runs || []).map((run) => ({
|
|
415
|
-
...run,
|
|
416
|
-
taskId: task.id,
|
|
417
|
-
taskTitle: task.title,
|
|
418
|
-
scheduleRule: task.scheduleRule,
|
|
419
|
-
projectName: task.projectName,
|
|
420
|
-
})))
|
|
408
|
+
const taskRunsService = createScheduledTaskRunsService({
|
|
409
|
+
readTasks: getTasks,
|
|
410
|
+
logger,
|
|
411
|
+
})
|
|
421
412
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (startedFrom && !Number.isNaN(startedFrom)) runs = runs.filter((run) => new Date(run.startedAt).getTime() >= startedFrom)
|
|
426
|
-
if (startedTo && !Number.isNaN(startedTo)) runs = runs.filter((run) => new Date(run.startedAt).getTime() <= startedTo)
|
|
427
|
-
if (keyword) runs = runs.filter((run) => runMatchesKeyword(run, keyword))
|
|
413
|
+
function authoritativeRun(task, runId) {
|
|
414
|
+
return task?.runs?.find((run) => run.id === runId) || null
|
|
415
|
+
}
|
|
428
416
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
417
|
+
async function persistRun(taskId, run, phase) {
|
|
418
|
+
if (!run?.id) return null
|
|
419
|
+
try {
|
|
420
|
+
return await taskRunsService.syncRun(taskId, run, { phase })
|
|
421
|
+
} catch (error) {
|
|
422
|
+
logger.warn('Scheduled task run persistence rejected', {
|
|
423
|
+
taskId,
|
|
424
|
+
runId: run.id,
|
|
425
|
+
phase,
|
|
426
|
+
errorName: error?.name || 'Error',
|
|
427
|
+
errorCode: error?.code,
|
|
428
|
+
})
|
|
429
|
+
throw error
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function syncAuthoritativeRun(task, runId, phase) {
|
|
434
|
+
const run = authoritativeRun(task, runId)
|
|
435
|
+
if (!run) return null
|
|
436
|
+
try {
|
|
437
|
+
return await persistRun(task.id, run, phase)
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (isScheduledRunsAuthoritative()) throw error
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function deleteAuthoritativeTaskRuns(taskId) {
|
|
445
|
+
try {
|
|
446
|
+
return await taskRunsService.deleteTaskRuns(taskId)
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (isScheduledRunsAuthoritative()) throw error
|
|
449
|
+
return false
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function getTaskRuns(url) {
|
|
454
|
+
const params = url.searchParams
|
|
455
|
+
return taskRunsService.listRuns({
|
|
456
|
+
page: params.get('page'),
|
|
457
|
+
pageSize: params.get('pageSize'),
|
|
458
|
+
taskId: params.get('taskId'),
|
|
459
|
+
status: params.get('status'),
|
|
460
|
+
trigger: params.get('trigger'),
|
|
461
|
+
keyword: params.get('keyword'),
|
|
462
|
+
startedFrom: params.get('startedFrom'),
|
|
463
|
+
startedTo: params.get('startedTo'),
|
|
464
|
+
})
|
|
433
465
|
}
|
|
434
466
|
|
|
435
467
|
async function updateTask(taskId, updater) {
|
|
436
468
|
let updated = null
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
469
|
+
try {
|
|
470
|
+
await atomicUpdate(STORE, (data) => {
|
|
471
|
+
if (!data[taskId]) return data
|
|
472
|
+
updated = updater(data[taskId])
|
|
473
|
+
if (isScheduledRunsAuthoritative() && updated) {
|
|
474
|
+
const { runs: _runs, ...metadata } = updated
|
|
475
|
+
updated = metadata
|
|
476
|
+
}
|
|
477
|
+
data[taskId] = updated
|
|
478
|
+
return data
|
|
479
|
+
})
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (isScheduledRunsAuthoritative()) {
|
|
482
|
+
try { recordScheduledRunsDiagnostic('task_metadata_update', error, { taskId }) } catch { /* Preserve metadata failure. */ }
|
|
483
|
+
}
|
|
484
|
+
throw error
|
|
485
|
+
}
|
|
443
486
|
return updated
|
|
444
487
|
}
|
|
445
488
|
|
|
@@ -495,40 +538,76 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
495
538
|
let agentWarning = null
|
|
496
539
|
let agentSnapshot = null
|
|
497
540
|
|
|
541
|
+
const createdRun = {
|
|
542
|
+
id: runId,
|
|
543
|
+
status: 'running',
|
|
544
|
+
trigger,
|
|
545
|
+
inputContent: task.instruction,
|
|
546
|
+
sessionId,
|
|
547
|
+
agentId: executionAgent?.id || task.agentId || null,
|
|
548
|
+
agentLabel: executionAgent?.label || null,
|
|
549
|
+
agentSnapshot,
|
|
550
|
+
warning: agentWarning || undefined,
|
|
551
|
+
scheduledAt,
|
|
552
|
+
startedAt,
|
|
553
|
+
}
|
|
554
|
+
let currentRun = createdRun
|
|
498
555
|
let started = false
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
556
|
+
let createdTask
|
|
557
|
+
if (isScheduledRunsAuthoritative()) {
|
|
558
|
+
await persistRun(task.id, createdRun, 'created')
|
|
559
|
+
try {
|
|
560
|
+
createdTask = await updateTask(task.id, (current) => {
|
|
561
|
+
const otherRunIds = activeRunIdsFor(current).filter((id) => id !== runId)
|
|
562
|
+
if (mode === 'serial' && otherRunIds.length > 0) return current
|
|
563
|
+
started = true
|
|
564
|
+
const nextRunAt = advanceNextRunAtAtStart
|
|
565
|
+
? calculateNextRun(current, new Date(startedAt))
|
|
566
|
+
: current.nextRunAt
|
|
567
|
+
const activeRunIds = appendCurrentRunId(current, runId)
|
|
568
|
+
return {
|
|
569
|
+
...current,
|
|
570
|
+
currentRunId: activeRunIds[activeRunIds.length - 1] || null,
|
|
571
|
+
currentRunIds: activeRunIds,
|
|
572
|
+
lastSessionId: sessionId,
|
|
573
|
+
nextRunAt,
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
} catch (error) {
|
|
577
|
+
try {
|
|
578
|
+
await taskRunsService.deleteRun(task.id, runId)
|
|
579
|
+
} catch (compensationError) {
|
|
580
|
+
throw new Error(`Scheduled run create metadata failed and SQLite compensation failed: ${compensationError?.message || compensationError}`, { cause: compensationError })
|
|
581
|
+
} finally {
|
|
582
|
+
removeActiveRun(task.id, runId)
|
|
583
|
+
}
|
|
584
|
+
throw error
|
|
526
585
|
}
|
|
527
|
-
|
|
586
|
+
if (!started) await taskRunsService.deleteRun(task.id, runId)
|
|
587
|
+
} else {
|
|
588
|
+
createdTask = await updateTask(task.id, (current) => {
|
|
589
|
+
const otherRunIds = activeRunIdsFor(current).filter((id) => id !== runId)
|
|
590
|
+
if (mode === 'serial' && otherRunIds.length > 0) return current
|
|
591
|
+
started = true
|
|
592
|
+
const nextRunAt = advanceNextRunAtAtStart
|
|
593
|
+
? calculateNextRun(current, new Date(startedAt))
|
|
594
|
+
: current.nextRunAt
|
|
595
|
+
const activeRunIds = appendCurrentRunId(current, runId)
|
|
596
|
+
return {
|
|
597
|
+
...current,
|
|
598
|
+
currentRunId: activeRunIds[activeRunIds.length - 1] || null,
|
|
599
|
+
currentRunIds: activeRunIds,
|
|
600
|
+
lastSessionId: sessionId,
|
|
601
|
+
nextRunAt,
|
|
602
|
+
runs: [createdRun, ...(current.runs || [])].slice(0, MAX_RUN_HISTORY_PER_TASK),
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
}
|
|
528
606
|
if (!started) {
|
|
529
607
|
removeActiveRun(task.id, runId)
|
|
530
608
|
return
|
|
531
609
|
}
|
|
610
|
+
if (!isScheduledRunsAuthoritative()) await syncAuthoritativeRun(createdTask, runId, 'created')
|
|
532
611
|
|
|
533
612
|
let settled = false
|
|
534
613
|
|
|
@@ -588,11 +667,23 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
588
667
|
message: truncateText(task.instruction, 500),
|
|
589
668
|
})
|
|
590
669
|
|
|
591
|
-
|
|
670
|
+
currentRun = {
|
|
671
|
+
...currentRun,
|
|
672
|
+
sessionId,
|
|
673
|
+
agentId: executionAgent?.id || task.agentId || null,
|
|
674
|
+
agentLabel: executionAgent?.label || null,
|
|
675
|
+
agentSnapshot,
|
|
676
|
+
warning: agentWarning || currentRun.warning,
|
|
677
|
+
}
|
|
678
|
+
const resolvedTask = await updateTask(task.id, (current) => ({
|
|
592
679
|
...current,
|
|
593
680
|
lastSessionId: sessionId,
|
|
594
|
-
|
|
681
|
+
...(!isScheduledRunsAuthoritative() ? {
|
|
682
|
+
runs: (current.runs || []).map((run) => run.id === runId ? { ...run, ...currentRun } : run),
|
|
683
|
+
} : {}),
|
|
595
684
|
}))
|
|
685
|
+
if (isScheduledRunsAuthoritative()) await persistRun(task.id, currentRun, 'resolved')
|
|
686
|
+
else await syncAuthoritativeRun(resolvedTask, runId, 'resolved')
|
|
596
687
|
onStarted?.({ taskId: task.id, runId, sessionId })
|
|
597
688
|
|
|
598
689
|
const eventBus = getSessionEventBus(sessionId)
|
|
@@ -681,30 +772,49 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
681
772
|
? (nextRunAt ? 'enabled' : 'completed')
|
|
682
773
|
: (recurring && nextRunAt ? 'enabled' : 'failed')
|
|
683
774
|
|
|
684
|
-
|
|
685
|
-
...
|
|
686
|
-
status:
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
775
|
+
const terminalRun = {
|
|
776
|
+
...currentRun,
|
|
777
|
+
status: result.aborted ? 'failed' : (result.ok ? 'success' : 'failed'),
|
|
778
|
+
inputContent: currentRun.inputContent ?? latestTask.instruction,
|
|
779
|
+
aiResult: result.ok ? aiResult : undefined,
|
|
780
|
+
result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
|
|
781
|
+
errorMessage: result.error,
|
|
782
|
+
sessionId,
|
|
783
|
+
agentId: executionAgent?.id || latestTask.agentId || null,
|
|
784
|
+
agentLabel: executionAgent?.label || null,
|
|
785
|
+
agentSnapshot,
|
|
786
|
+
warning: agentWarning || currentRun.warning,
|
|
787
|
+
finishedAt,
|
|
788
|
+
durationMs,
|
|
789
|
+
}
|
|
790
|
+
let terminalPersisted = false
|
|
791
|
+
if (isScheduledRunsAuthoritative()) {
|
|
792
|
+
await persistRun(task.id, terminalRun, 'terminal')
|
|
793
|
+
terminalPersisted = true
|
|
794
|
+
}
|
|
795
|
+
let terminalMetadataError = null
|
|
796
|
+
let terminalTask
|
|
797
|
+
try {
|
|
798
|
+
terminalTask = await updateTask(task.id, (current) => ({
|
|
799
|
+
...current,
|
|
800
|
+
status: nextStatus,
|
|
801
|
+
currentRunId: stillRunning ? remainingRunIds[remainingRunIds.length - 1] : null,
|
|
802
|
+
currentRunIds: remainingRunIds,
|
|
803
|
+
lastRunAt: finishedAt,
|
|
804
|
+
nextRunAt: nextRunAt ?? current.nextRunAt,
|
|
805
|
+
lastSessionId: sessionId,
|
|
806
|
+
...(!isScheduledRunsAuthoritative() ? {
|
|
807
|
+
runs: (current.runs || []).map((run) => run.id === runId ? terminalRun : run),
|
|
808
|
+
} : {}),
|
|
809
|
+
}))
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (!terminalPersisted) throw error
|
|
812
|
+
terminalMetadataError = error
|
|
813
|
+
try { recordScheduledRunsDiagnostic('terminal_metadata', error, { taskId: task.id, runId }) } catch { /* Preserve terminal success. */ }
|
|
814
|
+
logger.warn('Scheduled task terminal metadata persistence failed', { taskId: task.id, runId, errorName: error?.name || 'Error' })
|
|
815
|
+
}
|
|
816
|
+
if (!isScheduledRunsAuthoritative()) await syncAuthoritativeRun(terminalTask, runId, 'terminal')
|
|
817
|
+
if (terminalMetadataError) settled = true
|
|
708
818
|
|
|
709
819
|
emitScheduledTaskNotification({
|
|
710
820
|
task: latestTask,
|
|
@@ -718,7 +828,33 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
718
828
|
const finishedAt = new Date().toISOString()
|
|
719
829
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
720
830
|
removeActiveRun(task.id, runId)
|
|
721
|
-
|
|
831
|
+
const failureMessage = error?.message || String(error)
|
|
832
|
+
const failedRun = {
|
|
833
|
+
...currentRun,
|
|
834
|
+
status: 'failed',
|
|
835
|
+
errorMessage: failureMessage,
|
|
836
|
+
sessionId,
|
|
837
|
+
agentId: executionAgent?.id || task.agentId || null,
|
|
838
|
+
agentLabel: executionAgent?.label || null,
|
|
839
|
+
agentSnapshot,
|
|
840
|
+
warning: agentWarning || currentRun.warning,
|
|
841
|
+
finishedAt,
|
|
842
|
+
durationMs,
|
|
843
|
+
}
|
|
844
|
+
let persistenceError = null
|
|
845
|
+
if (isScheduledRunsAuthoritative()) {
|
|
846
|
+
try {
|
|
847
|
+
await persistRun(task.id, failedRun, 'exception-terminal')
|
|
848
|
+
} catch (terminalError) {
|
|
849
|
+
persistenceError = terminalError
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (persistenceError) {
|
|
853
|
+
try { recordScheduledRunsDiagnostic('exception_terminal', persistenceError, { taskId: task.id, runId }) } catch { /* Preserve the original failure. */ }
|
|
854
|
+
logger.error('Scheduled task terminal SQLite persistence failed', { taskId: task.id, runId, errorName: persistenceError?.name || 'Error' })
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
const failedTask = await updateTask(task.id, (current) => {
|
|
722
858
|
const remainingRunIds = removeCurrentRunId(current, runId)
|
|
723
859
|
const stillRunning = remainingRunIds.length > 0
|
|
724
860
|
return {
|
|
@@ -729,20 +865,12 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
729
865
|
lastRunAt: finishedAt,
|
|
730
866
|
lastSessionId: sessionId,
|
|
731
867
|
nextRunAt: stillRunning || advanceNextRunAtAtStart ? current.nextRunAt : (isRecurringTask(current) ? (calculateNextRun(current, new Date(finishedAt)) ?? current.nextRunAt) : current.nextRunAt),
|
|
732
|
-
|
|
733
|
-
...run,
|
|
734
|
-
|
|
735
|
-
errorMessage: error?.message || String(error),
|
|
736
|
-
sessionId,
|
|
737
|
-
agentId: executionAgent?.id || current.agentId || null,
|
|
738
|
-
agentLabel: executionAgent?.label || null,
|
|
739
|
-
agentSnapshot,
|
|
740
|
-
warning: agentWarning || run.warning,
|
|
741
|
-
finishedAt,
|
|
742
|
-
durationMs,
|
|
743
|
-
} : run),
|
|
868
|
+
...(!isScheduledRunsAuthoritative() ? {
|
|
869
|
+
runs: (current.runs || []).map((run) => run.id === runId ? { ...run, ...failedRun } : run),
|
|
870
|
+
} : {}),
|
|
744
871
|
}
|
|
745
872
|
})
|
|
873
|
+
if (!isScheduledRunsAuthoritative()) await syncAuthoritativeRun(failedTask, runId, 'exception-terminal')
|
|
746
874
|
emitScheduledTaskNotification({
|
|
747
875
|
task,
|
|
748
876
|
runId,
|
|
@@ -756,14 +884,52 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
756
884
|
}
|
|
757
885
|
}
|
|
758
886
|
|
|
887
|
+
export async function recoverStaleScheduledTaskRuns({ now = () => new Date() } = {}) {
|
|
888
|
+
const tasks = await getTasks()
|
|
889
|
+
const finishedAt = now().toISOString()
|
|
890
|
+
for (const task of tasks) {
|
|
891
|
+
const activeIds = currentRunIdsFor(task)
|
|
892
|
+
const sqliteRunning = isScheduledRunsAuthoritative()
|
|
893
|
+
? (await taskRunsService.listRuns({ taskId: task.id, status: 'running', page: 1, pageSize: MAX_RUN_HISTORY_PER_TASK })).runs
|
|
894
|
+
: []
|
|
895
|
+
const jsonRunning = Array.isArray(task.runs) ? task.runs.filter((run) => run?.status === 'running') : []
|
|
896
|
+
const runningById = new Map([...sqliteRunning, ...jsonRunning].map((run) => [run.id, run]))
|
|
897
|
+
for (const runId of activeIds) {
|
|
898
|
+
if (!runningById.has(runId)) runningById.set(runId, { id: runId, status: 'running', trigger: 'schedule', startedAt: task.lastRunAt || task.updatedAt || finishedAt })
|
|
899
|
+
}
|
|
900
|
+
if (runningById.size === 0 && activeIds.length === 0) continue
|
|
901
|
+
const failedRuns = []
|
|
902
|
+
for (const run of runningById.values()) {
|
|
903
|
+
const startedMs = new Date(run.startedAt).getTime()
|
|
904
|
+
const durationMs = Number.isFinite(startedMs) ? Math.max(0, new Date(finishedAt).getTime() - startedMs) : 0
|
|
905
|
+
const failed = { ...run, status: 'failed', errorMessage: 'Interrupted by previous process shutdown', finishedAt, durationMs }
|
|
906
|
+
failedRuns.push(failed)
|
|
907
|
+
if (isScheduledRunsAuthoritative()) await persistRun(task.id, failed, 'startup-recovery')
|
|
908
|
+
}
|
|
909
|
+
await updateTask(task.id, (current) => ({
|
|
910
|
+
...current,
|
|
911
|
+
currentRunId: null,
|
|
912
|
+
currentRunIds: [],
|
|
913
|
+
status: current.status === 'paused' ? 'paused' : (isRecurringTask(current) ? 'enabled' : 'failed'),
|
|
914
|
+
lastRunAt: finishedAt,
|
|
915
|
+
nextRunAt: isRecurringTask(current) ? (calculateNextRun(current, new Date(finishedAt)) ?? current.nextRunAt) : current.nextRunAt,
|
|
916
|
+
...(!isScheduledRunsAuthoritative() ? {
|
|
917
|
+
runs: (current.runs || []).map((run) => failedRuns.find((failed) => failed.id === run.id) || run),
|
|
918
|
+
} : {}),
|
|
919
|
+
}))
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
759
923
|
async function schedulerTick() {
|
|
760
|
-
if (running) return
|
|
924
|
+
if (running || !canStartScheduledRun()) return
|
|
761
925
|
running = true
|
|
762
926
|
try {
|
|
927
|
+
if (!canStartScheduledRun()) return
|
|
763
928
|
await repairRecurringTaskStatuses()
|
|
764
929
|
const now = Date.now()
|
|
765
930
|
const tasks = await getTasks()
|
|
766
931
|
for (const task of tasks) {
|
|
932
|
+
if (!canStartScheduledRun()) break
|
|
767
933
|
if (task.status !== 'enabled') continue
|
|
768
934
|
if (!task.nextRunAt || new Date(task.nextRunAt).getTime() > now) continue
|
|
769
935
|
if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) continue
|
|
@@ -774,23 +940,50 @@ async function schedulerTick() {
|
|
|
774
940
|
}
|
|
775
941
|
}
|
|
776
942
|
|
|
943
|
+
function requestSchedulerTick() {
|
|
944
|
+
if (tickPromise) return tickPromise
|
|
945
|
+
tickPromise = schedulerTick()
|
|
946
|
+
.catch((error) => logger.error('Scheduled task tick failed:', error))
|
|
947
|
+
.finally(() => { tickPromise = null })
|
|
948
|
+
return tickPromise
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function pauseScheduledTaskRunner() {
|
|
952
|
+
if (schedulerTimer) {
|
|
953
|
+
clearInterval(schedulerTimer)
|
|
954
|
+
schedulerTimer = null
|
|
955
|
+
}
|
|
956
|
+
if (tickPromise) await tickPromise
|
|
957
|
+
const tasks = await getTasks()
|
|
958
|
+
if (runningTaskRunIds.size > 0 || tasks.some(hasActiveTaskRuns)) {
|
|
959
|
+
const error = requestError('Cannot restore scheduled tasks while a run is active', 409)
|
|
960
|
+
error.errorCode = 'scheduled_runs_active'
|
|
961
|
+
throw error
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function resumeScheduledTaskRunner() {
|
|
966
|
+
if (runnerDesired) startScheduledTaskRunner()
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
configureScheduledRunsRuntimeHooks({ pause: pauseScheduledTaskRunner, resume: resumeScheduledTaskRunner })
|
|
970
|
+
|
|
777
971
|
export function startScheduledTaskRunner() {
|
|
778
|
-
|
|
779
|
-
schedulerTimer
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
schedulerTick().catch((error) => logger.error('Scheduled task initial tick failed:', error))
|
|
972
|
+
runnerDesired = true
|
|
973
|
+
if (schedulerTimer || !canStartScheduledRun()) return
|
|
974
|
+
schedulerTimer = setInterval(requestSchedulerTick, RUN_CHECK_INTERVAL_MS)
|
|
975
|
+
requestSchedulerTick()
|
|
783
976
|
}
|
|
784
977
|
|
|
785
978
|
export function stopScheduledTaskRunner() {
|
|
786
|
-
|
|
787
|
-
clearInterval(schedulerTimer)
|
|
979
|
+
runnerDesired = false
|
|
980
|
+
if (schedulerTimer) clearInterval(schedulerTimer)
|
|
788
981
|
schedulerTimer = null
|
|
789
|
-
runningTaskRunIds.clear()
|
|
790
982
|
}
|
|
791
983
|
|
|
792
984
|
export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
793
985
|
const parts = url.pathname.split('/').filter(Boolean)
|
|
986
|
+
assertScheduledRunsAvailable()
|
|
794
987
|
|
|
795
988
|
if (req.method === 'POST' && url.pathname === '/api/scheduled-tasks/parse') {
|
|
796
989
|
const body = await readJsonBody(req)
|
|
@@ -799,7 +992,7 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
799
992
|
}
|
|
800
993
|
|
|
801
994
|
if (req.method === 'GET' && url.pathname === '/api/scheduled-tasks') {
|
|
802
|
-
sendJson(res, 200, { tasks: await getTasks() })
|
|
995
|
+
sendJson(res, 200, { tasks: await hydrateTasksRuns(await getTasks()) })
|
|
803
996
|
return
|
|
804
997
|
}
|
|
805
998
|
|
|
@@ -829,13 +1022,13 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
829
1022
|
status: enabled ? 'enabled' : 'paused',
|
|
830
1023
|
createdAt: now,
|
|
831
1024
|
updatedAt: now,
|
|
832
|
-
runs: [],
|
|
1025
|
+
...(isScheduledRunsAuthoritative() ? {} : { runs: [] }),
|
|
833
1026
|
}
|
|
834
1027
|
await atomicUpdate(STORE, (data) => {
|
|
835
1028
|
data[task.id] = task
|
|
836
1029
|
return data
|
|
837
1030
|
})
|
|
838
|
-
sendJson(res, 200, { task })
|
|
1031
|
+
sendJson(res, 200, { task: await hydrateTaskRuns(task) })
|
|
839
1032
|
return
|
|
840
1033
|
}
|
|
841
1034
|
|
|
@@ -874,15 +1067,34 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
874
1067
|
status: parsed.enabled === false || parsed.status === 'paused' ? 'paused' : 'enabled',
|
|
875
1068
|
updatedAt: now,
|
|
876
1069
|
}))
|
|
877
|
-
sendJson(res, 200, { task })
|
|
1070
|
+
sendJson(res, 200, { task: await hydrateTaskRuns(task) })
|
|
878
1071
|
return
|
|
879
1072
|
}
|
|
880
1073
|
|
|
881
1074
|
if (req.method === 'DELETE' && !action) {
|
|
882
|
-
await
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1075
|
+
const existingForDelete = (await readStore(STORE))[taskId]
|
|
1076
|
+
const before = existingForDelete ? await hydrateTaskRuns(existingForDelete, MAX_RUN_HISTORY_PER_TASK) : null
|
|
1077
|
+
if (isScheduledRunsAuthoritative()) {
|
|
1078
|
+
await deleteAuthoritativeTaskRuns(taskId)
|
|
1079
|
+
try {
|
|
1080
|
+
await atomicUpdate(STORE, (data) => {
|
|
1081
|
+
delete data[taskId]
|
|
1082
|
+
return data
|
|
1083
|
+
})
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (before) {
|
|
1086
|
+
const restoredRuns = Array.isArray(before.runs) ? before.runs : []
|
|
1087
|
+
for (const run of restoredRuns) await persistRun(taskId, run, 'delete-compensation')
|
|
1088
|
+
}
|
|
1089
|
+
throw error
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
await atomicUpdate(STORE, (data) => {
|
|
1093
|
+
delete data[taskId]
|
|
1094
|
+
return data
|
|
1095
|
+
})
|
|
1096
|
+
await deleteAuthoritativeTaskRuns(taskId)
|
|
1097
|
+
}
|
|
886
1098
|
sendJson(res, 200, { ok: true })
|
|
887
1099
|
return
|
|
888
1100
|
}
|
|
@@ -891,7 +1103,7 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
891
1103
|
const current = (await readStore(STORE))[taskId]
|
|
892
1104
|
if (!current) throw requestError('Task not found', 404)
|
|
893
1105
|
const task = await updateTask(taskId, (latest) => ({ ...latest, status: 'paused', updatedAt: new Date().toISOString() }))
|
|
894
|
-
sendJson(res, 200, { task })
|
|
1106
|
+
sendJson(res, 200, { task: await hydrateTaskRuns(task) })
|
|
895
1107
|
return
|
|
896
1108
|
}
|
|
897
1109
|
|
|
@@ -905,7 +1117,7 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
905
1117
|
updatedAt: new Date().toISOString(),
|
|
906
1118
|
}))
|
|
907
1119
|
if (!task) throw requestError('Task not found', 404)
|
|
908
|
-
sendJson(res, 200, { task })
|
|
1120
|
+
sendJson(res, 200, { task: await hydrateTaskRuns(task) })
|
|
909
1121
|
return
|
|
910
1122
|
}
|
|
911
1123
|
|
|
@@ -914,14 +1126,15 @@ export async function handleScheduledTasksApi(req, res, url, context = {}) {
|
|
|
914
1126
|
const task = data[taskId]
|
|
915
1127
|
if (!task) throw requestError('Task not found', 404)
|
|
916
1128
|
if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) throw requestError('Task is already running', 409)
|
|
917
|
-
await new Promise((resolve) => {
|
|
1129
|
+
await new Promise((resolve, reject) => {
|
|
918
1130
|
executeTask(task, 'manual', resolve).catch((error) => {
|
|
919
1131
|
logger.error(`Manual scheduled task ${task.id} failed:`, error)
|
|
920
|
-
|
|
1132
|
+
if (isScheduledRunsAuthoritative()) reject(error)
|
|
1133
|
+
else resolve()
|
|
921
1134
|
})
|
|
922
1135
|
})
|
|
923
1136
|
const updatedTask = (await readStore(STORE))[taskId] ?? task
|
|
924
|
-
sendJson(res, 200, { ok: true, task: updatedTask })
|
|
1137
|
+
sendJson(res, 200, { ok: true, task: await hydrateTaskRuns(updatedTask) })
|
|
925
1138
|
return
|
|
926
1139
|
}
|
|
927
1140
|
}
|
package/server/routes/shares.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createConversationShare,
|
|
7
7
|
deleteConversationShare,
|
|
8
8
|
listConversationShares,
|
|
9
|
+
readConversationShare,
|
|
9
10
|
restoreConversationShare,
|
|
10
11
|
revokeConversationShare,
|
|
11
12
|
updateConversationShare,
|
|
@@ -102,8 +103,9 @@ export async function handleSharesApi(req, res, url, context = { isLocalRequest:
|
|
|
102
103
|
if (req.method === 'POST' && action === 'restore') {
|
|
103
104
|
const body = await readJsonBody(req)
|
|
104
105
|
const expiresAt = typeof body?.expiresAt === 'string' && body.expiresAt ? body.expiresAt : undefined
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Restore operates on the share record itself (including revoked state);
|
|
107
|
+
// the default list excludes revoked shares, so resolve directly.
|
|
108
|
+
const current = await readConversationShare(shareId)
|
|
107
109
|
if (!current) {
|
|
108
110
|
const error = new Error('Share not found')
|
|
109
111
|
error.statusCode = 404
|