mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Improvements to the queue processing.
This commit is contained in:
@@ -326,7 +326,16 @@ func (p *Processor) callClaudeCLI(ctx context.Context, prompt string) (string, e
|
|||||||
|
|
||||||
// Use claude CLI with --print flag for non-interactive output
|
// Use claude CLI with --print flag for non-interactive output
|
||||||
// and -p for prompt input
|
// and -p for prompt input
|
||||||
cmd := exec.CommandContext(ctx, p.claudePath, "--print", "-p", fullPrompt) // #nosec G204 -- claudePath is from config, fullPrompt is internal
|
// Add --tools "" to disable tools (we only need text analysis)
|
||||||
|
// Add --strict-mcp-config to skip loading MCP servers
|
||||||
|
// Add --disable-slash-commands to skip command loading
|
||||||
|
// These flags significantly speed up processing by avoiding plugin/MCP initialization
|
||||||
|
cmd := exec.CommandContext(ctx, p.claudePath,
|
||||||
|
"--print",
|
||||||
|
"--tools", "",
|
||||||
|
"--strict-mcp-config",
|
||||||
|
"--disable-slash-commands",
|
||||||
|
"-p", fullPrompt) // #nosec G204 -- claudePath is from config, fullPrompt is internal
|
||||||
|
|
||||||
// Set model if specified (use haiku for cost efficiency)
|
// Set model if specified (use haiku for cost efficiency)
|
||||||
if p.model != "" {
|
if p.model != "" {
|
||||||
|
|||||||
@@ -258,9 +258,22 @@ func (s *Service) initializeAsync() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set callback for session deletion
|
// Set callbacks for session lifecycle events
|
||||||
|
sessionManager.SetOnSessionCreated(func(id int64) {
|
||||||
|
s.broadcastProcessingStatus()
|
||||||
|
s.sseBroadcaster.Broadcast(map[string]interface{}{
|
||||||
|
"type": "session",
|
||||||
|
"action": "created",
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
})
|
||||||
sessionManager.SetOnSessionDeleted(func(id int64) {
|
sessionManager.SetOnSessionDeleted(func(id int64) {
|
||||||
s.broadcastProcessingStatus()
|
s.broadcastProcessingStatus()
|
||||||
|
s.sseBroadcaster.Broadcast(map[string]interface{}{
|
||||||
|
"type": "session",
|
||||||
|
"action": "deleted",
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mark as ready
|
// Mark as ready
|
||||||
@@ -448,9 +461,22 @@ func (s *Service) reinitializeDatabase() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set callback for session deletion
|
// Set callbacks for session lifecycle events
|
||||||
|
sessionManager.SetOnSessionCreated(func(id int64) {
|
||||||
|
s.broadcastProcessingStatus()
|
||||||
|
s.sseBroadcaster.Broadcast(map[string]interface{}{
|
||||||
|
"type": "session",
|
||||||
|
"action": "created",
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
})
|
||||||
sessionManager.SetOnSessionDeleted(func(id int64) {
|
sessionManager.SetOnSessionDeleted(func(id int64) {
|
||||||
s.broadcastProcessingStatus()
|
s.broadcastProcessingStatus()
|
||||||
|
s.sseBroadcaster.Broadcast(map[string]interface{}{
|
||||||
|
"type": "session",
|
||||||
|
"action": "deleted",
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mark as ready again
|
// Mark as ready again
|
||||||
@@ -719,6 +745,7 @@ func (s *Service) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processQueue processes the observation queue in the background.
|
// processQueue processes the observation queue in the background.
|
||||||
|
// Processes immediately when notified, or every QueueProcessInterval as fallback.
|
||||||
func (s *Service) processQueue() {
|
func (s *Service) processQueue() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
|
|
||||||
@@ -729,7 +756,11 @@ func (s *Service) processQueue() {
|
|||||||
select {
|
select {
|
||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
return
|
return
|
||||||
|
case <-s.sessionManager.ProcessNotify:
|
||||||
|
// Immediate processing when observation is queued
|
||||||
|
s.processAllSessions()
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
// Fallback periodic processing
|
||||||
s.processAllSessions()
|
s.processAllSessions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,20 +62,88 @@ type ActiveSession struct {
|
|||||||
generatorActive atomic.Bool
|
generatorActive atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionTimeout is how long an inactive session can exist before cleanup.
|
||||||
|
const SessionTimeout = 30 * time.Minute
|
||||||
|
|
||||||
|
// CleanupInterval is how often to check for stale sessions.
|
||||||
|
const CleanupInterval = 5 * time.Minute
|
||||||
|
|
||||||
// Manager manages active session lifecycles.
|
// Manager manages active session lifecycles.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
sessionStore *sqlite.SessionStore
|
sessionStore *sqlite.SessionStore
|
||||||
sessions map[int64]*ActiveSession
|
sessions map[int64]*ActiveSession
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
onCreated func(int64)
|
||||||
onDeleted func(int64)
|
onDeleted func(int64)
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
// Global notification channel for immediate processing
|
||||||
|
ProcessNotify chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new session manager.
|
// NewManager creates a new session manager.
|
||||||
func NewManager(sessionStore *sqlite.SessionStore) *Manager {
|
func NewManager(sessionStore *sqlite.SessionStore) *Manager {
|
||||||
return &Manager{
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
m := &Manager{
|
||||||
sessionStore: sessionStore,
|
sessionStore: sessionStore,
|
||||||
sessions: make(map[int64]*ActiveSession),
|
sessions: make(map[int64]*ActiveSession),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
ProcessNotify: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
|
// Start background cleanup goroutine
|
||||||
|
go m.cleanupLoop()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupLoop periodically removes stale sessions.
|
||||||
|
func (m *Manager) cleanupLoop() {
|
||||||
|
ticker := time.NewTicker(CleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
m.cleanupStaleSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupStaleSessions removes sessions that have been inactive too long.
|
||||||
|
func (m *Manager) cleanupStaleSessions() {
|
||||||
|
m.mu.RLock()
|
||||||
|
var staleIDs []int64
|
||||||
|
now := time.Now()
|
||||||
|
for id, session := range m.sessions {
|
||||||
|
// Check if session has been inactive for too long
|
||||||
|
session.messageMu.Lock()
|
||||||
|
hasPending := len(session.pendingMessages) > 0
|
||||||
|
session.messageMu.Unlock()
|
||||||
|
|
||||||
|
// Don't delete sessions with pending messages or active processing
|
||||||
|
if hasPending || session.generatorActive.Load() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete if session is older than timeout
|
||||||
|
if now.Sub(session.StartTime) > SessionTimeout {
|
||||||
|
staleIDs = append(staleIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
// Delete stale sessions
|
||||||
|
for _, id := range staleIDs {
|
||||||
|
log.Info().Int64("sessionId", id).Dur("age", SessionTimeout).Msg("Cleaning up stale session")
|
||||||
|
m.DeleteSession(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnSessionCreated sets a callback for when a session is created.
|
||||||
|
func (m *Manager) SetOnSessionCreated(callback func(int64)) {
|
||||||
|
m.onCreated = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOnSessionDeleted sets a callback for when a session is deleted.
|
// SetOnSessionDeleted sets a callback for when a session is deleted.
|
||||||
@@ -86,7 +154,6 @@ func (m *Manager) SetOnSessionDeleted(callback func(int64)) {
|
|||||||
// InitializeSession initializes a session, creating it if needed.
|
// InitializeSession initializes a session, creating it if needed.
|
||||||
func (m *Manager) InitializeSession(ctx context.Context, sessionDBID int64, userPrompt string, promptNumber int) (*ActiveSession, error) {
|
func (m *Manager) InitializeSession(ctx context.Context, sessionDBID int64, userPrompt string, promptNumber int) (*ActiveSession, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
// Check if already active
|
// Check if already active
|
||||||
if session, ok := m.sessions[sessionDBID]; ok {
|
if session, ok := m.sessions[sessionDBID]; ok {
|
||||||
@@ -95,10 +162,12 @@ func (m *Manager) InitializeSession(ctx context.Context, sessionDBID int64, user
|
|||||||
session.UserPrompt = userPrompt
|
session.UserPrompt = userPrompt
|
||||||
session.LastPromptNumber = promptNumber
|
session.LastPromptNumber = promptNumber
|
||||||
}
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch from database
|
// Fetch from database (unlock during DB call to avoid blocking)
|
||||||
|
m.mu.Unlock()
|
||||||
dbSession, err := m.sessionStore.GetSessionByID(ctx, sessionDBID)
|
dbSession, err := m.sessionStore.GetSessionByID(ctx, sessionDBID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -135,7 +204,17 @@ func (m *Manager) InitializeSession(ctx context.Context, sessionDBID int64, user
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-acquire lock to add session
|
||||||
|
m.mu.Lock()
|
||||||
|
// Double-check another goroutine didn't create it
|
||||||
|
if existing, ok := m.sessions[sessionDBID]; ok {
|
||||||
|
m.mu.Unlock()
|
||||||
|
cancel() // Clean up unused context
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
m.sessions[sessionDBID] = session
|
m.sessions[sessionDBID] = session
|
||||||
|
onCreated := m.onCreated
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Int64("sessionId", sessionDBID).
|
Int64("sessionId", sessionDBID).
|
||||||
@@ -143,6 +222,11 @@ func (m *Manager) InitializeSession(ctx context.Context, sessionDBID int64, user
|
|||||||
Str("claudeSessionId", session.ClaudeSessionID).
|
Str("claudeSessionId", session.ClaudeSessionID).
|
||||||
Msg("Session initialized")
|
Msg("Session initialized")
|
||||||
|
|
||||||
|
// Notify callback (outside lock)
|
||||||
|
if onCreated != nil {
|
||||||
|
onCreated(sessionDBID)
|
||||||
|
}
|
||||||
|
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +254,18 @@ func (m *Manager) QueueObservation(ctx context.Context, sessionDBID int64, data
|
|||||||
queueDepth := len(session.pendingMessages)
|
queueDepth := len(session.pendingMessages)
|
||||||
session.messageMu.Unlock()
|
session.messageMu.Unlock()
|
||||||
|
|
||||||
// Non-blocking notification
|
// Non-blocking notification to session
|
||||||
select {
|
select {
|
||||||
case session.notify <- struct{}{}:
|
case session.notify <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-blocking notification to global processor
|
||||||
|
select {
|
||||||
|
case m.ProcessNotify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Int64("sessionId", sessionDBID).
|
Int64("sessionId", sessionDBID).
|
||||||
Str("tool", data.ToolName).
|
Str("tool", data.ToolName).
|
||||||
@@ -212,12 +302,18 @@ func (m *Manager) QueueSummarize(ctx context.Context, sessionDBID int64, lastUse
|
|||||||
queueDepth := len(session.pendingMessages)
|
queueDepth := len(session.pendingMessages)
|
||||||
session.messageMu.Unlock()
|
session.messageMu.Unlock()
|
||||||
|
|
||||||
// Non-blocking notification
|
// Non-blocking notification to session
|
||||||
select {
|
select {
|
||||||
case session.notify <- struct{}{}:
|
case session.notify <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-blocking notification to global processor
|
||||||
|
select {
|
||||||
|
case m.ProcessNotify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Int64("sessionId", sessionDBID).
|
Int64("sessionId", sessionDBID).
|
||||||
Int("queueDepth", queueDepth).
|
Int("queueDepth", queueDepth).
|
||||||
@@ -255,6 +351,9 @@ func (m *Manager) DeleteSession(sessionDBID int64) {
|
|||||||
|
|
||||||
// ShutdownAll shuts down all active sessions.
|
// ShutdownAll shuts down all active sessions.
|
||||||
func (m *Manager) ShutdownAll(ctx context.Context) {
|
func (m *Manager) ShutdownAll(ctx context.Context) {
|
||||||
|
// Stop cleanup goroutine
|
||||||
|
m.cancel()
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
sessionIDs := make([]int64, 0, len(m.sessions))
|
sessionIDs := make([]int64, 0, len(m.sessions))
|
||||||
for id := range m.sessions {
|
for id := range m.sessions {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-5-gcd14ba8-dirty",
|
"version": "v0.6.1-7-gf0c35ee-dirty",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-5-gcd14ba8-dirty",
|
"version": "v0.6.1-7-gf0c35ee-dirty",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-5-gcd14ba8-dirty",
|
"version": "v0.6.1-7-gf0c35ee-dirty",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+2
-8
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
|
||||||
import { useSSE, useStats, useTimeline, useUpdate, useHealth } from '@/composables'
|
import { useSSE, useStats, useTimeline, useUpdate, useHealth } from '@/composables'
|
||||||
import Header from '@/components/Header.vue'
|
import Header from '@/components/Header.vue'
|
||||||
import StatsCards from '@/components/StatsCards.vue'
|
import StatsCards from '@/components/StatsCards.vue'
|
||||||
@@ -8,7 +7,7 @@ import Timeline from '@/components/Timeline.vue'
|
|||||||
import Sidebar from '@/components/Sidebar.vue'
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
const { isConnected, isProcessing, queueDepth, lastEvent } = useSSE()
|
const { isConnected, isProcessing, queueDepth } = useSSE()
|
||||||
const { stats } = useStats()
|
const { stats } = useStats()
|
||||||
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
||||||
const { health } = useHealth()
|
const { health } = useHealth()
|
||||||
@@ -29,12 +28,7 @@ const {
|
|||||||
setConceptFilter
|
setConceptFilter
|
||||||
} = useTimeline()
|
} = useTimeline()
|
||||||
|
|
||||||
// Refresh timeline when new events arrive
|
// Note: Timeline refresh is handled by useTimeline's SSE watcher
|
||||||
watch(lastEvent, (event) => {
|
|
||||||
if (event && (event.type === 'observation' || event.type === 'prompt')) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import type { SSEEvent } from '@/types'
|
import type { SSEEvent } from '@/types'
|
||||||
|
|
||||||
export function useSSE() {
|
// Singleton state - shared across all useSSE() calls
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
const isProcessing = ref(false)
|
const isProcessing = ref(false)
|
||||||
const queueDepth = ref(0)
|
const queueDepth = ref(0)
|
||||||
const lastEvent = ref<SSEEvent | null>(null)
|
const lastEvent = ref<SSEEvent | null>(null)
|
||||||
|
|
||||||
let eventSource: EventSource | null = null
|
let eventSource: EventSource | null = null
|
||||||
let reconnectTimeout: number | null = null
|
let reconnectTimeout: number | null = null
|
||||||
|
let connectionCount = 0
|
||||||
|
|
||||||
|
export function useSSE() {
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
|
// Only create connection if not already connected
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
eventSource = new EventSource('/api/events')
|
eventSource = new EventSource('/api/events')
|
||||||
@@ -25,6 +29,12 @@ export function useSSE() {
|
|||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data: SSEEvent = JSON.parse(event.data)
|
const data: SSEEvent = JSON.parse(event.data)
|
||||||
|
|
||||||
|
// Debug: log all SSE events
|
||||||
|
if (data.type !== 'processing_status') {
|
||||||
|
console.log('[SSE] Event received:', data.type, data)
|
||||||
|
}
|
||||||
|
|
||||||
lastEvent.value = data
|
lastEvent.value = data
|
||||||
|
|
||||||
if (data.type === 'processing_status') {
|
if (data.type === 'processing_status') {
|
||||||
@@ -82,18 +92,25 @@ export function useSSE() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Add listeners to close SSE on page refresh/navigation
|
connectionCount++
|
||||||
|
if (connectionCount === 1) {
|
||||||
|
// First consumer - add listeners and connect
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
window.addEventListener('pagehide', handlePageHide)
|
window.addEventListener('pagehide', handlePageHide)
|
||||||
window.addEventListener('pageshow', handlePageShow)
|
window.addEventListener('pageshow', handlePageShow)
|
||||||
connect()
|
connect()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
connectionCount--
|
||||||
|
if (connectionCount === 0) {
|
||||||
|
// Last consumer - remove listeners and disconnect
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||||
window.removeEventListener('pagehide', handlePageHide)
|
window.removeEventListener('pagehide', handlePageHide)
|
||||||
window.removeEventListener('pageshow', handlePageShow)
|
window.removeEventListener('pageshow', handlePageShow)
|
||||||
disconnect()
|
disconnect()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import type { Stats } from '@/types'
|
import type { Stats } from '@/types'
|
||||||
import { fetchStats } from '@/utils/api'
|
import { fetchStats } from '@/utils/api'
|
||||||
|
import { useSSE } from './useSSE'
|
||||||
|
|
||||||
export function useStats(pollInterval: number = 5000) {
|
export function useStats(pollInterval: number = 5000) {
|
||||||
const stats = ref<Stats | null>(null)
|
const stats = ref<Stats | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
// SSE for real-time session updates
|
||||||
|
const { lastEvent } = useSSE()
|
||||||
|
|
||||||
let intervalId: number | null = null
|
let intervalId: number | null = null
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
@@ -35,6 +39,14 @@ export function useStats(pollInterval: number = 5000) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for SSE session events and refresh immediately
|
||||||
|
watch(lastEvent, (event) => {
|
||||||
|
if (event && event.type === 'session') {
|
||||||
|
console.log('[Stats] SSE session event triggered refresh:', event.action)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refresh()
|
refresh()
|
||||||
startPolling()
|
startPolling()
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export function useTimeline() {
|
|||||||
// Watch for SSE events and refresh
|
// Watch for SSE events and refresh
|
||||||
watch(lastEvent, (event) => {
|
watch(lastEvent, (event) => {
|
||||||
if (event && (event.type === 'observation' || event.type === 'prompt' || event.type === 'summary')) {
|
if (event && (event.type === 'observation' || event.type === 'prompt' || event.type === 'summary')) {
|
||||||
|
console.log('[Timeline] SSE event triggered refresh:', event.type)
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user