mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): add debug trace instrumentation
A device-local toggle (Settings → Advanced → Debug trace logging) that, when enabled, dumps structured console.debug entries for every stateful event in the desktop app: - Session state transitions (busy/needsInput/storedSessionId edges) - Compaction start/finish per session - message.complete events (session id, message count, usage/billing) - Session switches (activeSessionId + selectedStoredSessionId changes) - Compression id rotation (the spookiest bug class — route/pin/draft key silently changes mid-turn) - Persistence writes (key, op, truncated value preview) - All gateway events (type, session id, payload — deltas summarized) - Gateway connection state (idle→open→closed) + connection mode/profile - Profile switches ($activeGatewayProfile changes) - Resume failures + exhaustion (the #1 'stuck on loading' signal) - Busy/awaitingResponse edges - Message array length changes (count only, not per-token) - Error boundary catches (React render crashes with componentStack) - Blocking prompts: clarify/approval/sudo/secret raised/cleared edges - Sessions list length changes (new/archive/delete/merge) Zero cost when disabled: every debugTrace() call early-returns on the $debugTraceEnabled atom, and the subscriptions (persistence, gateway events, atom watchers) are no-op closures when tracing is off. Pattern follows keep-awake: device-local localStorage atom, side-effect import in main.tsx, ToggleRow in Settings → Advanced. i18n strings in all four locales (en/ja/zh/zh-hant).
This commit is contained in:
parent
d44674fe08
commit
a71a90bc73
12 changed files with 386 additions and 4 deletions
|
|
@ -11,6 +11,7 @@ import { translateNow } from '@/i18n'
|
|||
import { type GatewayEventPayload, textPart } from '@/lib/chat-messages'
|
||||
import { coerceGatewayText, coerceThinkingText, normalizePersonalityValue } from '@/lib/chat-runtime'
|
||||
import { playCompletionSound } from '@/lib/completion-sound'
|
||||
import { debugTrace } from '@/lib/debug-trace'
|
||||
import { resolveGatewayEventSessionId } from '@/lib/gateway-events'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { modelOptionsQueryKey } from '@/lib/model-options'
|
||||
|
|
@ -34,6 +35,7 @@ import {
|
|||
$currentCwd,
|
||||
$currentModel,
|
||||
$currentProvider,
|
||||
$messages,
|
||||
sessionMatchesStoredId,
|
||||
setCurrentBranch,
|
||||
setCurrentCwd,
|
||||
|
|
@ -541,6 +543,17 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
return
|
||||
}
|
||||
|
||||
debugTrace(
|
||||
'message',
|
||||
`complete session=${sessionId}`,
|
||||
{
|
||||
isActive: isActiveEvent,
|
||||
messageCount: $messages.get().length,
|
||||
hasUsage: Boolean(payload?.usage),
|
||||
hasBilling: Boolean(payload?.billing)
|
||||
}
|
||||
)
|
||||
|
||||
// Turn ended — drop any blocking prompt still open for THIS session
|
||||
// (e.g. interrupted, or the approval already resolved). Scoped to the
|
||||
// session so a background turn finishing can't wipe the active chat's
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useSearchParams } from 'react-router-dom'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { getElevenLabsVoices, getHermesConfigSchema, saveHermesConfig } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { $debugTraceEnabled, setDebugTraceEnabled } from '@/lib/debug-trace'
|
||||
import { $keepAwake, setKeepAwake } from '@/store/keep-awake'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { repoDiscoveryPolicyFromConfig, repoDiscoveryPolicySignature, scanAndRecordRepos } from '@/store/projects'
|
||||
|
|
@ -57,6 +58,7 @@ export function ConfigSettings({
|
|||
const { t } = useI18n()
|
||||
const c = t.settings.config
|
||||
const keepAwake = useStore($keepAwake)
|
||||
const debugTraceEnabled = useStore($debugTraceEnabled)
|
||||
// The editable draft is local (debounced autosave watches it), but it's seeded
|
||||
// from — and saved back through — the shared config cache, so edits are visible
|
||||
// in the MCP/model surfaces and reopening the page doesn't reload-flash.
|
||||
|
|
@ -293,6 +295,9 @@ export function ConfigSettings({
|
|||
{activeSectionId === 'advanced' && (
|
||||
<ToggleRow checked={keepAwake} description={c.keepAwakeDesc} label={c.keepAwakeTitle} onChange={setKeepAwake} />
|
||||
)}
|
||||
{activeSectionId === 'advanced' && (
|
||||
<ToggleRow checked={debugTraceEnabled} description={c.debugTraceDesc} label={c.debugTraceTitle} onChange={setDebugTraceEnabled} />
|
||||
)}
|
||||
{visibleFields.length === 0 ? (
|
||||
<EmptyState description={c.emptyDesc} title={c.emptyTitle} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ErrorState } from '@/components/ui/error-state'
|
||||
import { debugTrace } from '@/lib/debug-trace'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export interface ErrorBoundaryFallbackProps {
|
||||
|
|
@ -30,6 +31,10 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
const tag = this.props.label ? `[error-boundary:${this.props.label}]` : '[error-boundary]'
|
||||
console.error(tag, error, info.componentStack)
|
||||
debugTrace('error-boundary', `${this.props.label ?? 'root'}: ${error.message}`, {
|
||||
error,
|
||||
componentStack: info.componentStack
|
||||
})
|
||||
this.props.onError?.(error, info)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -541,7 +541,9 @@ export const en: Translations = {
|
|||
imported: 'Config imported',
|
||||
invalidJson: 'Invalid config JSON',
|
||||
keepAwakeTitle: 'Keep computer awake',
|
||||
keepAwakeDesc: 'Stop this machine from sleeping so long or overnight runs keep going. The display can still dim.'
|
||||
keepAwakeDesc: 'Stop this machine from sleeping so long or overnight runs keep going. The display can still dim.',
|
||||
debugTraceTitle: 'Debug trace logging',
|
||||
debugTraceDesc: 'Log verbose state transitions, session switches, compaction events, and gateway events to the devtools console for bug reproduction.'
|
||||
},
|
||||
credentials: {
|
||||
pasteKey: 'Paste key',
|
||||
|
|
|
|||
|
|
@ -648,7 +648,9 @@ export const ja = defineLocale({
|
|||
imported: '設定をインポートしました',
|
||||
invalidJson: '設定 JSON が無効です',
|
||||
keepAwakeTitle: 'コンピューターをスリープさせない',
|
||||
keepAwakeDesc: '本体のスリープを防ぎ、長時間や夜通しの実行を継続します。画面は暗転できます。'
|
||||
keepAwakeDesc: '本体のスリープを防ぎ、長時間や夜通しの実行を継続します。画面は暗転できます。',
|
||||
debugTraceTitle: 'デバッグトレースログ',
|
||||
debugTraceDesc: 'バグ再現用に、状態遷移・セッション切替・圧縮イベント・ゲートウェイイベントをdevtoolsコンソールに詳細出力します。'
|
||||
},
|
||||
credentials: {
|
||||
pasteKey: 'キーを貼り付け',
|
||||
|
|
|
|||
|
|
@ -448,6 +448,8 @@ export interface Translations {
|
|||
invalidJson: string
|
||||
keepAwakeTitle: string
|
||||
keepAwakeDesc: string
|
||||
debugTraceTitle: string
|
||||
debugTraceDesc: string
|
||||
}
|
||||
credentials: {
|
||||
pasteKey: string
|
||||
|
|
|
|||
|
|
@ -636,7 +636,9 @@ export const zhHant = defineLocale({
|
|||
imported: '設定已匯入',
|
||||
invalidJson: '設定 JSON 無效',
|
||||
keepAwakeTitle: '保持電腦喚醒',
|
||||
keepAwakeDesc: '阻止本機睡眠,讓長時間或整夜執行持續進行。螢幕仍可變暗。'
|
||||
keepAwakeDesc: '阻止本機睡眠,讓長時間或整夜執行持續進行。螢幕仍可變暗。',
|
||||
debugTraceTitle: '除錯追蹤日誌',
|
||||
debugTraceDesc: '將狀態轉換、工作階段切換、壓縮事件和閘道事件詳細輸出到 devtools 控制台,用於重現 bug。'
|
||||
},
|
||||
credentials: {
|
||||
pasteKey: '貼上金鑰',
|
||||
|
|
|
|||
|
|
@ -748,7 +748,9 @@ export const zh: Translations = {
|
|||
imported: '配置已导入',
|
||||
invalidJson: '配置 JSON 无效',
|
||||
keepAwakeTitle: '保持电脑唤醒',
|
||||
keepAwakeDesc: '阻止本机休眠,让长时间或通宵运行继续进行。屏幕仍可变暗。'
|
||||
keepAwakeDesc: '阻止本机休眠,让长时间或通宵运行继续进行。屏幕仍可变暗。',
|
||||
debugTraceTitle: '调试追踪日志',
|
||||
debugTraceDesc: '将状态转换、会话切换、压缩事件和网关事件详细输出到 devtools 控制台,用于复现 bug。'
|
||||
},
|
||||
credentials: {
|
||||
pasteKey: '粘贴密钥',
|
||||
|
|
|
|||
328
apps/desktop/src/lib/debug-trace.ts
Normal file
328
apps/desktop/src/lib/debug-trace.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
/**
|
||||
* Debug trace — optional, verbose instrumentation of stateful desktop events.
|
||||
*
|
||||
* When enabled (Settings → Advanced), dumps structured `console.debug` entries
|
||||
* for every session state transition, compaction event, message completion,
|
||||
* session switch, persistence write, and gateway event. The output lands in the
|
||||
* renderer devtools console and any attached log capture, so you can point at
|
||||
* the trace after reproducing a bug.
|
||||
*
|
||||
* Zero cost when disabled: every call site is a single function call that
|
||||
* early-returns when the atom is off. The subscriptions (persistence, gateway
|
||||
* events, atom watchers) are only attached once at module init, and their
|
||||
* callbacks also early-return — so the overhead is one closure call per event,
|
||||
* which is negligible.
|
||||
*/
|
||||
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
import { onGatewayEvent } from '@/contrib/events'
|
||||
import { onPersistenceEvent } from '@/lib/storage'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$activeSessionStoredIdRotation,
|
||||
$awaitingResponse,
|
||||
$busy,
|
||||
$connection,
|
||||
$gatewayState,
|
||||
$messages,
|
||||
$resumeExhaustedSessionId,
|
||||
$resumeFailedSessionId,
|
||||
$selectedStoredSessionId,
|
||||
$sessions
|
||||
} from '@/store/session'
|
||||
import { $clarifyRequest } from '@/store/clarify'
|
||||
import { $approvalRequest, $secretRequest, $sudoRequest } from '@/store/prompts'
|
||||
|
||||
const KEY = 'hermes.desktop.debugTrace.v1'
|
||||
|
||||
/** Device-local preference — off by default, per machine. */
|
||||
export const $debugTraceEnabled = atom<boolean>(
|
||||
typeof window === 'undefined' ? false : (() => {
|
||||
try {
|
||||
return window.localStorage.getItem(KEY) === 'true'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
)
|
||||
|
||||
export function setDebugTraceEnabled(on: boolean): void {
|
||||
$debugTraceEnabled.set(on)
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
$debugTraceEnabled.subscribe(on => {
|
||||
try {
|
||||
window.localStorage.setItem(KEY, String(on))
|
||||
} catch {
|
||||
// Storage best-effort.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export type DebugCategory =
|
||||
| 'session-state'
|
||||
| 'compaction'
|
||||
| 'message'
|
||||
| 'session-switch'
|
||||
| 'persistence'
|
||||
| 'gateway-event'
|
||||
| 'connection'
|
||||
| 'profile-switch'
|
||||
| 'resume'
|
||||
| 'busy'
|
||||
| 'error-boundary'
|
||||
| 'prompt'
|
||||
| 'sessions-list'
|
||||
|
||||
const CATEGORY_PREFIX: Record<DebugCategory, string> = {
|
||||
'session-state': '[trace:session-state]',
|
||||
compaction: '[trace:compaction]',
|
||||
message: '[trace:message]',
|
||||
'session-switch': '[trace:session-switch]',
|
||||
persistence: '[trace:persistence]',
|
||||
'gateway-event': '[trace:gateway-event]',
|
||||
connection: '[trace:connection]',
|
||||
'profile-switch': '[trace:profile-switch]',
|
||||
resume: '[trace:resume]',
|
||||
busy: '[trace:busy]',
|
||||
'error-boundary': '[trace:error-boundary]',
|
||||
prompt: '[trace:prompt]',
|
||||
'sessions-list': '[trace:sessions-list]'
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a debug trace entry. No-ops entirely when tracing is disabled.
|
||||
*
|
||||
* `data` is spread as additional console arguments (not stringified) so
|
||||
* devtools can expand/inspect objects natively.
|
||||
*/
|
||||
export function debugTrace(
|
||||
category: DebugCategory,
|
||||
message: string,
|
||||
...data: unknown[]
|
||||
): void {
|
||||
if (!$debugTraceEnabled.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
const ts = new Date().toISOString()
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`${CATEGORY_PREFIX[category]} ${ts} ${message}`, ...data)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Subscriptions — attached once at module init, no-op when disabled.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// --- Session switches ---
|
||||
let prevActive: string | null = null
|
||||
$activeSessionId.subscribe(id => {
|
||||
if (id !== prevActive) {
|
||||
debugTrace('session-switch', `activeSessionId ${prevActive ?? 'null'} → ${id ?? 'null'}`)
|
||||
prevActive = id
|
||||
}
|
||||
})
|
||||
|
||||
let prevSelected: string | null = null
|
||||
$selectedStoredSessionId.subscribe(id => {
|
||||
if (id !== prevSelected) {
|
||||
debugTrace('session-switch', `selectedStoredSessionId ${prevSelected ?? 'null'} → ${id ?? 'null'}`)
|
||||
prevSelected = id
|
||||
}
|
||||
})
|
||||
|
||||
// --- Persistence events ---
|
||||
onPersistenceEvent(event => {
|
||||
const valuePreview =
|
||||
event.value === null
|
||||
? 'null'
|
||||
: event.value.length > 120
|
||||
? `${event.value.slice(0, 120)}…(${event.value.length} chars)`
|
||||
: event.value
|
||||
|
||||
debugTrace('persistence', `${event.op} ${event.key}`, { value: valuePreview })
|
||||
})
|
||||
|
||||
// --- Gateway events (wildcard) ---
|
||||
onGatewayEvent('*', event => {
|
||||
const type = event.type ?? 'unknown'
|
||||
const raw = event as unknown as Record<string, unknown>
|
||||
const sessionId = raw.session_id ?? raw.sessionId ?? null
|
||||
|
||||
// Summarize — don't dump the full payload for high-frequency deltas.
|
||||
const summary: Record<string, unknown> = { type }
|
||||
|
||||
if (sessionId) {
|
||||
summary.sessionId = sessionId
|
||||
}
|
||||
|
||||
// For message.delta, just note it happened (they fire 30×/s during a turn).
|
||||
// For everything else, include the payload for inspection.
|
||||
if (type === 'message.delta') {
|
||||
summary.note = 'delta (streaming)'
|
||||
} else {
|
||||
summary.payload = event
|
||||
}
|
||||
|
||||
debugTrace('gateway-event', type, summary)
|
||||
})
|
||||
|
||||
// --- Gateway connection state ---
|
||||
let prevGatewayState: string | undefined
|
||||
$gatewayState.subscribe(state => {
|
||||
if (state !== prevGatewayState) {
|
||||
debugTrace('connection', `gatewayState ${prevGatewayState ?? 'undefined'} → ${state}`)
|
||||
prevGatewayState = state
|
||||
}
|
||||
})
|
||||
|
||||
// --- Connection (mode/baseUrl/profile) ---
|
||||
let prevConnMode: string | undefined
|
||||
let prevConnProfile: string | undefined
|
||||
$connection.subscribe(conn => {
|
||||
const mode = conn?.mode ?? 'null'
|
||||
const profile = conn?.profile ?? 'null'
|
||||
|
||||
if (mode !== prevConnMode || profile !== prevConnProfile) {
|
||||
debugTrace('connection', `connection mode=${mode} profile=${profile} baseUrl=${conn?.baseUrl ?? 'null'}`)
|
||||
prevConnMode = mode
|
||||
prevConnProfile = profile
|
||||
}
|
||||
})
|
||||
|
||||
// --- Profile switches ---
|
||||
let prevProfile: string | undefined
|
||||
$activeGatewayProfile.subscribe(profile => {
|
||||
if (profile !== prevProfile) {
|
||||
debugTrace('profile-switch', `${prevProfile ?? 'undefined'} → ${profile}`)
|
||||
prevProfile = profile
|
||||
}
|
||||
})
|
||||
|
||||
// --- Resume failures + exhaustion ---
|
||||
let prevResumeFailed: string | null = null
|
||||
$resumeFailedSessionId.subscribe(id => {
|
||||
if (id !== prevResumeFailed) {
|
||||
debugTrace('resume', `resumeFailedSessionId ${prevResumeFailed ?? 'null'} → ${id ?? 'null'}`)
|
||||
prevResumeFailed = id
|
||||
}
|
||||
})
|
||||
|
||||
let prevResumeExhausted: string | null = null
|
||||
$resumeExhaustedSessionId.subscribe(id => {
|
||||
if (id !== prevResumeExhausted) {
|
||||
debugTrace('resume', `resumeExhaustedSessionId ${prevResumeExhausted ?? 'null'} → ${id ?? 'null'}`)
|
||||
prevResumeExhausted = id
|
||||
}
|
||||
})
|
||||
|
||||
// --- Busy / awaitingResponse edges ---
|
||||
let prevBusy = false
|
||||
$busy.subscribe(busy => {
|
||||
if (busy !== prevBusy) {
|
||||
debugTrace('busy', `busy ${prevBusy} → ${busy}`, { activeSessionId: $activeSessionId.get() })
|
||||
prevBusy = busy
|
||||
}
|
||||
})
|
||||
|
||||
let prevAwaiting = false
|
||||
$awaitingResponse.subscribe(awaiting => {
|
||||
if (awaiting !== prevAwaiting) {
|
||||
debugTrace('busy', `awaitingResponse ${prevAwaiting} → ${awaiting}`, { activeSessionId: $activeSessionId.get() })
|
||||
prevAwaiting = awaiting
|
||||
}
|
||||
})
|
||||
|
||||
// --- Message array length changes ---
|
||||
// Not per-token (that'd be insane) — only when the count changes, which
|
||||
// captures: new message added, transcript cleared, session switched,
|
||||
// reconciliation replaced the array.
|
||||
let prevMessageCount = -1
|
||||
$messages.subscribe(messages => {
|
||||
const count = messages.length
|
||||
|
||||
if (count !== prevMessageCount) {
|
||||
debugTrace('message', `messages count ${prevMessageCount} → ${count}`, {
|
||||
activeSessionId: $activeSessionId.get()
|
||||
})
|
||||
prevMessageCount = count
|
||||
}
|
||||
})
|
||||
|
||||
// --- Compression id rotation ---
|
||||
// Fires when auto-compaction rotates the active session's stored id mid-turn.
|
||||
// One of the spookiest bug classes — the route / pin / draft key silently
|
||||
// changes under the user.
|
||||
$activeSessionStoredIdRotation.subscribe(rotation => {
|
||||
if (rotation) {
|
||||
debugTrace('session-switch', `compression id rotation`, {
|
||||
prev: rotation.previousStoredSessionId,
|
||||
next: rotation.nextStoredSessionId,
|
||||
runtime: rotation.runtimeSessionId,
|
||||
isActive: rotation.runtimeSessionId === $activeSessionId.get()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// --- Blocking prompts (clarify / approval / sudo / secret) ---
|
||||
// When these appear/disappear, the chat is blocked. Tracing the edges
|
||||
// catches "agent silently stalled" bugs.
|
||||
let prevClarify: unknown = null
|
||||
$clarifyRequest.subscribe(req => {
|
||||
if (req !== prevClarify) {
|
||||
debugTrace('prompt', `clarify ${prevClarify ? 'cleared' : 'raised'}`, {
|
||||
sessionId: $activeSessionId.get(),
|
||||
requestId: req ? 'present' : 'null'
|
||||
})
|
||||
prevClarify = req
|
||||
}
|
||||
})
|
||||
|
||||
let prevApproval: unknown = null
|
||||
$approvalRequest.subscribe(req => {
|
||||
if (req !== prevApproval) {
|
||||
debugTrace('prompt', `approval ${prevApproval ? 'cleared' : 'raised'}`, {
|
||||
sessionId: $activeSessionId.get()
|
||||
})
|
||||
prevApproval = req
|
||||
}
|
||||
})
|
||||
|
||||
let prevSudo: unknown = null
|
||||
$sudoRequest.subscribe(req => {
|
||||
if (req !== prevSudo) {
|
||||
debugTrace('prompt', `sudo ${prevSudo ? 'cleared' : 'raised'}`, {
|
||||
sessionId: $activeSessionId.get()
|
||||
})
|
||||
prevSudo = req
|
||||
}
|
||||
})
|
||||
|
||||
let prevSecret: unknown = null
|
||||
$secretRequest.subscribe(req => {
|
||||
if (req !== prevSecret) {
|
||||
debugTrace('prompt', `secret ${prevSecret ? 'cleared' : 'raised'}`, {
|
||||
sessionId: $activeSessionId.get()
|
||||
})
|
||||
prevSecret = req
|
||||
}
|
||||
})
|
||||
|
||||
// --- Sessions list length changes ---
|
||||
// Captures: new session created, session archived/deleted, sidebar merge
|
||||
// kept/dropped a row. Not per-field — just the count edge.
|
||||
let prevSessionsCount = -1
|
||||
$sessions.subscribe(list => {
|
||||
const count = list.length
|
||||
|
||||
if (count !== prevSessionsCount) {
|
||||
debugTrace('sessions-list', `sessions count ${prevSessionsCount} → ${count}`)
|
||||
prevSessionsCount = count
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import './styles.css'
|
||||
// Side-effect: applies the persisted window translucency on load.
|
||||
import './store/translucency'
|
||||
// Side-effect: attaches debug trace subscriptions (persistence, gateway
|
||||
// events, session switch watchers). No-ops entirely when tracing is disabled.
|
||||
import './lib/debug-trace'
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { StrictMode } from 'react'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { atom, computed } from 'nanostores'
|
||||
|
||||
import { debugTrace } from '@/lib/debug-trace'
|
||||
import { $activeSessionId } from './session'
|
||||
|
||||
// Per-session flag while auto-compaction runs mid-turn. Without it the
|
||||
|
|
@ -25,6 +26,8 @@ export function setSessionCompacting(sessionId: string | null | undefined, activ
|
|||
|
||||
$compactingSessions.set({ ...sessions, [key]: true })
|
||||
|
||||
debugTrace('compaction', `started session=${key}`, { isActive: key === $activeSessionId.get() })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -35,4 +38,6 @@ export function setSessionCompacting(sessionId: string | null | undefined, activ
|
|||
const next = { ...sessions }
|
||||
delete next[key]
|
||||
$compactingSessions.set(next)
|
||||
|
||||
debugTrace('compaction', `finished session=${key}`, { isActive: key === $activeSessionId.get() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
noteActiveTreeGroup,
|
||||
revealTreePane
|
||||
} from '@/components/pane-shell/tree/store'
|
||||
import { debugTrace } from '@/lib/debug-trace'
|
||||
import { stableArray } from '@/lib/stable-array'
|
||||
import { readJson, writeJson } from '@/lib/storage'
|
||||
|
||||
|
|
@ -189,6 +190,18 @@ export function publishSessionState(runtimeId: string, state: ClientSessionState
|
|||
const prev = $sessionStates.get()[runtimeId] ?? null
|
||||
$sessionStates.set({ ...$sessionStates.get(), [runtimeId]: state })
|
||||
handleTransition(prev, state, runtimeId)
|
||||
|
||||
debugTrace(
|
||||
'session-state',
|
||||
`publish ${runtimeId}`,
|
||||
{
|
||||
prev: prev
|
||||
? { storedSessionId: prev.storedSessionId, busy: prev.busy, needsInput: prev.needsInput }
|
||||
: null,
|
||||
next: { storedSessionId: state.storedSessionId, busy: state.busy, needsInput: state.needsInput },
|
||||
isActive: runtimeId === $activeSessionId.get()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function dropSessionState(runtimeId: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue