From c690d0c4836e77fdfa07087a58544d021f4c10f8 Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Fri, 27 Mar 2026 18:32:25 +0800 Subject: [PATCH] Hardcode agent stale timeout --- app/api/routes_config.py | 2 -- app/config.py | 3 +-- app/database.py | 2 +- app/models.py | 2 -- app/services/config_service.py | 5 ----- app/services/status_service.py | 13 ++--------- go-server/internal/api/config.go | 6 +----- go-server/internal/api/status.go | 3 +-- go-server/internal/config/config.go | 3 +-- go-server/internal/db/db.go | 4 ++-- go-server/internal/models/models.go | 1 - go-server/internal/store/settings.go | 9 +------- static/css/layout.css | 5 ----- static/index.html | 16 +++----------- static/js/status.js | 32 +--------------------------- 15 files changed, 14 insertions(+), 92 deletions(-) diff --git a/app/api/routes_config.py b/app/api/routes_config.py index 5a95684..fa7f2ed 100644 --- a/app/api/routes_config.py +++ b/app/api/routes_config.py @@ -27,14 +27,12 @@ def update_config(body: UpdateConfigRequest): cfg = config_service.update_config( default_wait_seconds=body.default_wait_seconds, default_empty_response=body.default_empty_response, - agent_stale_after_seconds=body.agent_stale_after_seconds, ) event_service.broadcast( "config.updated", { "default_wait_seconds": cfg.default_wait_seconds, "default_empty_response": cfg.default_empty_response, - "agent_stale_after_seconds": cfg.agent_stale_after_seconds, }, ) return cfg diff --git a/app/config.py b/app/config.py index f30eb93..c3db7ba 100644 --- a/app/config.py +++ b/app/config.py @@ -11,6 +11,7 @@ from dataclasses import dataclass, field APP_VERSION = "1.0.1" DEFAULT_EMPTY_RESPONSE = "call this tool `get_user_request` again to fetch latest user input..." +AGENT_STALE_AFTER_SECONDS = 30 @dataclass @@ -28,7 +29,6 @@ class Settings: # MCP / queue behaviour (runtime-editable values are stored in DB; these are defaults for first run) default_wait_seconds: int = 10 default_empty_response: str = DEFAULT_EMPTY_RESPONSE - agent_stale_after_seconds: int = 30 # MCP server name mcp_server_name: str = "local-mcp" @@ -58,7 +58,6 @@ def load_settings() -> Settings: log_level=os.getenv("LOG_LEVEL", "INFO"), default_wait_seconds=int(os.getenv("DEFAULT_WAIT_SECONDS", "10")), default_empty_response=os.getenv("DEFAULT_EMPTY_RESPONSE", DEFAULT_EMPTY_RESPONSE), - agent_stale_after_seconds=int(os.getenv("AGENT_STALE_AFTER_SECONDS", "30")), mcp_server_name=os.getenv("MCP_SERVER_NAME", "local-mcp"), mcp_stateless=_parse_bool(os.getenv("MCP_STATELESS", "true"), default=True), api_token=os.getenv("API_TOKEN", ""), diff --git a/app/database.py b/app/database.py index 17b1a7a..872d1a7 100644 --- a/app/database.py +++ b/app/database.py @@ -105,7 +105,6 @@ CREATE TABLE IF NOT EXISTS agent_activity ( _DEFAULT_SETTINGS = { "default_wait_seconds": "10", "default_empty_response": DEFAULT_EMPTY_RESPONSE, - "agent_stale_after_seconds": "30", } @@ -125,6 +124,7 @@ def _seed_defaults(conn: sqlite3.Connection) -> None: "UPDATE settings SET value = ? WHERE key = 'default_empty_response' AND value = ''", (_DEFAULT_SETTINGS["default_empty_response"],), ) + conn.execute("DELETE FROM settings WHERE key = 'agent_stale_after_seconds'") conn.commit() logger.debug("Default settings seeded") diff --git a/app/models.py b/app/models.py index 23f42cb..cf1b34c 100644 --- a/app/models.py +++ b/app/models.py @@ -109,13 +109,11 @@ class StatusResponse(BaseModel): class ConfigResponse(BaseModel): default_wait_seconds: int default_empty_response: str - agent_stale_after_seconds: int class UpdateConfigRequest(BaseModel): default_wait_seconds: Optional[int] = None default_empty_response: Optional[str] = None - agent_stale_after_seconds: Optional[int] = None # --------------------------------------------------------------------------- diff --git a/app/services/config_service.py b/app/services/config_service.py index 9ff2379..6d684cc 100644 --- a/app/services/config_service.py +++ b/app/services/config_service.py @@ -13,7 +13,6 @@ from app.models import ConfigResponse logger = logging.getLogger(__name__) -_SETTING_KEYS = {"default_wait_seconds", "default_empty_response", "agent_stale_after_seconds"} def get_config() -> ConfigResponse: with get_conn() as conn: rows = conn.execute("SELECT key, value FROM settings").fetchall() @@ -21,22 +20,18 @@ def get_config() -> ConfigResponse: return ConfigResponse( default_wait_seconds=int(data.get("default_wait_seconds", 10)), default_empty_response=data.get("default_empty_response") or DEFAULT_EMPTY_RESPONSE, - agent_stale_after_seconds=int(data.get("agent_stale_after_seconds", 30)), ) def update_config( default_wait_seconds: int | None = None, default_empty_response: str | None = None, - agent_stale_after_seconds: int | None = None, ) -> ConfigResponse: updates: dict[str, str] = {} if default_wait_seconds is not None: updates["default_wait_seconds"] = str(default_wait_seconds) if default_empty_response is not None: updates["default_empty_response"] = default_empty_response - if agent_stale_after_seconds is not None: - updates["agent_stale_after_seconds"] = str(agent_stale_after_seconds) if updates: with get_write_conn() as conn: diff --git a/app/services/status_service.py b/app/services/status_service.py index 507a59d..b98a8ef 100644 --- a/app/services/status_service.py +++ b/app/services/status_service.py @@ -10,6 +10,7 @@ import sqlite3 from datetime import datetime, timezone from typing import Optional +from app.config import AGENT_STALE_AFTER_SECONDS from app.database import get_conn, get_write_conn logger = logging.getLogger(__name__) @@ -55,25 +56,15 @@ def get_latest_agent_activity() -> Optional[sqlite3.Row]: ).fetchone() -def get_agent_stale_seconds() -> int: - """Read agent_stale_after_seconds from settings table.""" - with get_conn() as conn: - row = conn.execute( - "SELECT value FROM settings WHERE key = 'agent_stale_after_seconds'" - ).fetchone() - return int(row["value"]) if row else 30 - - def is_agent_connected() -> bool: """True if the most recent agent activity is within the stale threshold.""" row = get_latest_agent_activity() if row is None: return False - stale_seconds = get_agent_stale_seconds() last_seen = datetime.fromisoformat(row["last_seen_at"]) now = datetime.now(timezone.utc) if last_seen.tzinfo is None: last_seen = last_seen.replace(tzinfo=timezone.utc) delta = (now - last_seen).total_seconds() - return delta <= stale_seconds + return delta <= AGENT_STALE_AFTER_SECONDS diff --git a/go-server/internal/api/config.go b/go-server/internal/api/config.go index d15a918..2e49c29 100644 --- a/go-server/internal/api/config.go +++ b/go-server/internal/api/config.go @@ -22,8 +22,7 @@ func handleUpdateConfig(stores Stores, broker *events.Broker) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Decode partial patch var patch struct { - DefaultWaitSeconds *int `json:"default_wait_seconds"` - AgentStaleAfterSeconds *int `json:"agent_stale_after_seconds"` + DefaultWaitSeconds *int `json:"default_wait_seconds"` } if err := json.NewDecoder(r.Body).Decode(&patch); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") @@ -41,9 +40,6 @@ func handleUpdateConfig(stores Stores, broker *events.Broker) http.HandlerFunc { if patch.DefaultWaitSeconds != nil { current.DefaultWaitSeconds = *patch.DefaultWaitSeconds } - if patch.AgentStaleAfterSeconds != nil { - current.AgentStaleAfterSeconds = *patch.AgentStaleAfterSeconds - } if err := stores.Settings.Update(current); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) diff --git a/go-server/internal/api/status.go b/go-server/internal/api/status.go index 9561bf8..fb902cf 100644 --- a/go-server/internal/api/status.go +++ b/go-server/internal/api/status.go @@ -45,7 +45,7 @@ func handleStatus(stores Stores) http.HandlerFunc { } if latest != nil { - connected := time.Since(latest.LastSeenAt).Seconds() <= float64(cfg.AgentStaleAfterSeconds) + connected := time.Since(latest.LastSeenAt).Seconds() <= float64(config.AgentStaleAfterSeconds) agent = map[string]any{ "connected": connected, "last_seen_at": latest.LastSeenAt.Format(time.RFC3339Nano), @@ -69,7 +69,6 @@ func handleStatus(stores Stores) http.HandlerFunc { "settings": models.Settings{ DefaultWaitSeconds: cfg.DefaultWaitSeconds, DefaultEmptyResponse: cfg.DefaultEmptyResponse, - AgentStaleAfterSeconds: cfg.AgentStaleAfterSeconds, }, } diff --git a/go-server/internal/config/config.go b/go-server/internal/config/config.go index 3e7ce16..2de1961 100644 --- a/go-server/internal/config/config.go +++ b/go-server/internal/config/config.go @@ -8,6 +8,7 @@ import ( const ( AppVersion = "1.0.1" + AgentStaleAfterSeconds = 30 DefaultEmptyResponse = "call this tool `get_user_request` again to fetch latest user input..." ) @@ -18,7 +19,6 @@ type Config struct { DBPath string LogLevel string DefaultWaitSeconds int - AgentStaleAfterSeconds int MCPStateless bool APIToken string } @@ -31,7 +31,6 @@ func Load() Config { DBPath: getEnv("DB_PATH", "data/local_mcp.sqlite3"), LogLevel: getEnv("LOG_LEVEL", "INFO"), DefaultWaitSeconds: getEnvInt("DEFAULT_WAIT_SECONDS", 10), - AgentStaleAfterSeconds: getEnvInt("AGENT_STALE_AFTER_SECONDS", 30), MCPStateless: getEnvBool("MCP_STATELESS", true), APIToken: getEnv("API_TOKEN", ""), } diff --git a/go-server/internal/db/db.go b/go-server/internal/db/db.go index 2e99e85..f905b7d 100644 --- a/go-server/internal/db/db.go +++ b/go-server/internal/db/db.go @@ -42,8 +42,8 @@ CREATE TABLE IF NOT EXISTS agent_activity ( // defaultSettings seeds initial values; OR IGNORE means existing rows are unchanged. const defaultSettings = "" + "INSERT OR IGNORE INTO settings (key, value) VALUES ('default_wait_seconds', '10');\n" + - "INSERT OR IGNORE INTO settings (key, value) VALUES ('agent_stale_after_seconds', '30');\n" + - "DELETE FROM settings WHERE key = 'default_empty_response';\n" + "DELETE FROM settings WHERE key = 'default_empty_response';\n" + + "DELETE FROM settings WHERE key = 'agent_stale_after_seconds';\n" // Open opens (creating if necessary) a SQLite database at dbPath, applies the // schema, and seeds default settings. diff --git a/go-server/internal/models/models.go b/go-server/internal/models/models.go index c63693e..dc07379 100644 --- a/go-server/internal/models/models.go +++ b/go-server/internal/models/models.go @@ -27,7 +27,6 @@ type Instruction struct { type Settings struct { DefaultWaitSeconds int `json:"default_wait_seconds"` DefaultEmptyResponse string `json:"default_empty_response"` - AgentStaleAfterSeconds int `json:"agent_stale_after_seconds"` } // AgentActivity tracks the last time an agent called get_user_request. diff --git a/go-server/internal/store/settings.go b/go-server/internal/store/settings.go index 9a6bcc0..3aa458b 100644 --- a/go-server/internal/store/settings.go +++ b/go-server/internal/store/settings.go @@ -30,7 +30,6 @@ func (s *SettingsStore) Get() (models.Settings, error) { cfg := models.Settings{ DefaultWaitSeconds: 10, DefaultEmptyResponse: config.DefaultEmptyResponse, - AgentStaleAfterSeconds: 30, } for rows.Next() { @@ -43,10 +42,6 @@ func (s *SettingsStore) Get() (models.Settings, error) { if n, err := strconv.Atoi(value); err == nil { cfg.DefaultWaitSeconds = n } - case "agent_stale_after_seconds": - if n, err := strconv.Atoi(value); err == nil { - cfg.AgentStaleAfterSeconds = n - } } } return cfg, rows.Err() @@ -57,10 +52,8 @@ func (s *SettingsStore) Get() (models.Settings, error) { func (s *SettingsStore) Update(patch models.Settings) error { _, err := s.db.Exec(` INSERT OR REPLACE INTO settings (key, value) VALUES - ('default_wait_seconds', ?), - ('agent_stale_after_seconds', ?)`, + ('default_wait_seconds', ?)` , strconv.Itoa(patch.DefaultWaitSeconds), - strconv.Itoa(patch.AgentStaleAfterSeconds), ) return err } diff --git a/static/css/layout.css b/static/css/layout.css index 3e9e843..bd9c2eb 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -37,11 +37,6 @@ color: var(--cyan); } -.header-version-badge { - letter-spacing: 0.08em; - text-transform: uppercase; -} - .header-indicators { display: flex; align-items: center; diff --git a/static/index.html b/static/index.html index a016570..9508640 100644 --- a/static/index.html +++ b/static/index.html @@ -30,8 +30,6 @@ local-mcp - server - v–
@@ -159,17 +157,9 @@
-
-
- - - Inactivity before agent shown as idle -
- -
+

+ Agent connectivity is now inferred with a fixed 30-second timeout managed by the server. +

diff --git a/static/js/status.js b/static/js/status.js index c512f9f..85f815d 100644 --- a/static/js/status.js +++ b/static/js/status.js @@ -5,8 +5,6 @@ */ import { state } from './state.js'; -import { api } from './api.js'; -import { toast } from './app.js'; // ── Time helpers ────────────────────────────────────────────────────────── @@ -126,35 +124,7 @@ export function initStatus() { // ── Config panel ────────────────────────────────────────────────────────── export function initConfig() { - const form = document.getElementById('config-form'); - const staleInput = document.getElementById('cfg-stale'); - const saveBtn = document.getElementById('cfg-save'); - - // Populate from state - state.subscribe('config', (cfg) => { - if (!cfg) return; - staleInput.value = cfg.agent_stale_after_seconds; - }); - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - saveBtn.disabled = true; - const original = saveBtn.innerHTML; - saveBtn.innerHTML = ''; - - try { - const cfg = await api.updateConfig({ - agent_stale_after_seconds: parseInt(staleInput.value, 10) || 30, - }); - state.set('config', cfg); - toast('Settings saved', 'success'); - } catch (e) { - toast(e.message, 'error'); - } finally { - saveBtn.disabled = false; - saveBtn.innerHTML = original; - } - }); + // Intentionally empty: the web UI no longer exposes editable settings. } function escapeHtml(str) {