From 4988ede4d4b53f2cca64a962731de22c188a23f7 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Thu, 29 Oct 2026 00:14:01 +0300 Subject: [PATCH 1/2] feat(observe): close idle sessions The sweep ends an active session (status 'ended', ended_at = its last activity) once none of its observations is open and nothing in it started or finished within the idle window (`observe start --session-idle-minutes N`, default 30, 0 = off). New activity past ended_at reopens an ended or interrupted session on ingest; stale or reordered events do not. Retention then prunes ended sessions like any other finished one. Studio's Threads status facet offers 'ended'. Co-Authored-By: Claude Opus 5.5 --- .../src/screens/threads/ThreadsScreen.tsx | 8 +- .../threads/__tests__/threads.test.tsx | 9 ++ docs/cli.md | 11 +- docs/observability.md | 28 +++- llms-full.txt | 39 +++++- src/cli/observe.test.ts | 4 + src/cli/observe.ts | 37 ++++-- src/observe/daemon.ts | 17 ++- src/observe/store.test.ts | 61 +++++++++ src/observe/store.ts | 39 +++++- src/observe/sweep.test.ts | 125 ++++++++++++++++++ src/observe/sweep.ts | 34 +++++ 12 files changed, 393 insertions(+), 19 deletions(-) diff --git a/apps/agent-forge/src/screens/threads/ThreadsScreen.tsx b/apps/agent-forge/src/screens/threads/ThreadsScreen.tsx index 7f8d39a64..27e229945 100644 --- a/apps/agent-forge/src/screens/threads/ThreadsScreen.tsx +++ b/apps/agent-forge/src/screens/threads/ThreadsScreen.tsx @@ -34,8 +34,12 @@ import { type ThreadsUrlState, } from './threadsUrl'; -/** Session statuses the daemon writes: `active`, then `interrupted` when the agent's crash sweep closes it. */ -const STATUSES = ['active', 'interrupted']; +/** + * Session statuses the daemon writes: `active`; `ended` once its idle sweep sees + * nothing run for the idle window; `interrupted` when the agent's crash sweep + * closes it. New activity makes an ended or interrupted session `active` again. + */ +const STATUSES = ['active', 'ended', 'interrupted']; /** The session's span, start to its latest finished observation; `-` while nothing has finished. */ const span = (s: ObserveSessionRow): number | null => (s.ended_at === null ? null : s.ended_at - s.started_at); diff --git a/apps/agent-forge/src/screens/threads/__tests__/threads.test.tsx b/apps/agent-forge/src/screens/threads/__tests__/threads.test.tsx index 2e17d4ba0..bdf80bcfa 100644 --- a/apps/agent-forge/src/screens/threads/__tests__/threads.test.tsx +++ b/apps/agent-forge/src/screens/threads/__tests__/threads.test.tsx @@ -147,6 +147,15 @@ describe('ThreadsScreen list', () => { expect(custom?.getAttribute('aria-pressed')).toBe('true'); }); + it('offers the ended status the idle sweep writes', async () => { + const container = await mountAt('#/threads?status=ended'); + expect(listRequests().at(-1)!.get('status')).toBe('ended'); + const statusBtn = [...container.querySelectorAll('.facet-btn')].find((b) => b.textContent?.startsWith('Status'))!; + act(() => statusBtn.click()); + const options = [...container.querySelectorAll('.facet-pop .facet-opt')].map((o) => o.textContent); + expect(options).toEqual(['active', 'ended', 'interrupted']); + }); + it('opening a row navigates to #/threads/:sessionId', async () => { const container = await mountAt('#/threads?status=active'); const row = container.querySelector('tbody tr[data-row-index="0"]')!; diff --git a/docs/cli.md b/docs/cli.md index 591d8752c..ba051535b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -36,7 +36,7 @@ lousho mcp [--http --port N --host H] lousho doctor [agent.yaml|json|agent-dir|agent.ts] [--json] [--ping] lousho eval [globs...] [--tag t] [--junit path] [--json path] [--strict] [--judge] [--record | --replay | --drift [--drift-usage]] [--url [--token ]] [--config vitest.config.ts] [--timeout ] lousho traces [traceId|prefix] [--dir D] [--limit N] [--json] [--content] -lousho observe [--port N] [--db path] [--host H] [--require-token] [--project X] [--webhook URL] [--retention-days N] [--older-than d] [--since d|h|m] [--agent X] [--out file] +lousho observe [--port N] [--db path] [--host H] [--require-token] [--project X] [--webhook URL] [--retention-days N] [--session-idle-minutes N] [--older-than d] [--since d|h|m] [--agent X] [--out file] lousho prompt |list|get |label