Add restart staleness warnings and resource listing improvements

write_file notes when agent.md or command files change; lazy stderr
warning (once per class per lifetime); /status reports stale fields;
resource listing adds parent entries for modules/ and connections/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
bernatsampera
2026-08-10 09:27:35 +02:00
co-authored by Claude Opus 4.6
parent 07c8b76906
commit d7226fce03
4 changed files with 151 additions and 6 deletions
+28
View File
@@ -121,6 +121,33 @@ def _index_warning(root: Path, target: Path, content: str, existed: bool) -> str
return ""
def _restart_note(root: Path, target: Path) -> str:
"""Note text for files that only load at server start, or '' otherwise.
agent.md is pushed in the MCP handshake and command files register as
prompts at startup; a write through this tool takes effect only after a
restart. Advisory only, same contract as _index_warning.
"""
parts = target.relative_to(root.resolve()).parts
if parts == ("agent.md",):
return (
" Note: agent.md is pushed at connect; this change reaches clients "
"only after a restart (stop the server, gcontext up, reconnect the client)."
)
if (
len(parts) == 4
and parts[0] in ("connections", "modules")
and parts[2] == "commands"
and target.suffix in (".md", ".py")
):
return (
" Note: commands are registered at server start; this command appears "
"(or updates) only after a restart (stop the server, gcontext up, "
"reconnect the client)."
)
return ""
DIFF_MAX_LINES = 200
@@ -163,6 +190,7 @@ def write_file(root: Path, path: str, content: str) -> str:
return (
line
+ _index_warning(root, target, content, existed)
+ _restart_note(root, target)
+ (_write_diff(path, before, content) if existed else "")
)
+77 -6
View File
@@ -51,6 +51,61 @@ def _tool_doc(name: str) -> str:
# Live MCP sessions, keyed by session id: {"client": ..., "connected": ..., "last_seen": ...}
SESSIONS: dict[str, dict] = {}
# Two file classes load only at server start: agent.md (pushed in the MCP
# handshake) and command files (registered as prompts). No watchers, per the
# no-background-behavior design: a startup snapshot of their mtimes, compared
# lazily on tool calls, with one stderr line per class per server lifetime.
STARTUP_SNAPSHOT: dict = {"agent_md": None, "commands": {}}
_STALE = {"agent_md": False, "commands": False}
_STALE_WARNED = {"agent_md": False, "commands": False}
_STALE_CHECK_INTERVAL = 5.0
_last_stale_check = 0.0
def _mtime(path: Path) -> float | None:
try:
return path.stat().st_mtime
except OSError:
return None
def snapshot_startup_files():
"""Record the state of the start-time-loaded files. Call once, after
load_instructions() and register_commands() have run."""
STARTUP_SNAPSHOT["agent_md"] = _mtime(PROJECT_DIR / "agent.md")
STARTUP_SNAPSHOT["commands"] = {
str(p): _mtime(p) for p in commands_mod.discover(PROJECT_DIR)
}
_STALE.update(agent_md=False, commands=False)
_STALE_WARNED.update(agent_md=False, commands=False)
def check_staleness(force: bool = False) -> dict:
"""Compare the current files against the startup snapshot.
Once a class is stale it stays stale until restart, so the comparison for
it stops. Throttled to one filesystem check per few seconds unless forced.
"""
global _last_stale_check
now = time.monotonic()
if not force and now - _last_stale_check < _STALE_CHECK_INTERVAL:
return dict(_STALE)
_last_stale_check = now
if not _STALE["agent_md"]:
_STALE["agent_md"] = _mtime(PROJECT_DIR / "agent.md") != STARTUP_SNAPSHOT["agent_md"]
if not _STALE["commands"]:
current = {str(p): _mtime(p) for p in commands_mod.discover(PROJECT_DIR)}
_STALE["commands"] = current != STARTUP_SNAPSHOT["commands"]
if _STALE["agent_md"] and not _STALE_WARNED["agent_md"]:
_STALE_WARNED["agent_md"] = True
print(" ! agent.md changed since start; restart to push the new version "
"(stop, gcontext up, reconnect the client)", file=sys.stderr)
if _STALE["commands"] and not _STALE_WARNED["commands"]:
_STALE_WARNED["commands"] = True
print(" ! commands changed since start; restart to re-register them",
file=sys.stderr)
return dict(_STALE)
# Activity feed for the dashboard: in-memory ring buffer, gone on restart.
EVENTS: deque = deque(maxlen=300)
_EVENT_SEQ = itertools.count(1)
@@ -125,6 +180,7 @@ class ConnectionTracker(Middleware):
return await call_next(context)
async def on_call_tool(self, context, call_next):
check_staleness()
name = getattr(context.message, "name", "?")
arguments = getattr(context.message, "arguments", None) or {}
detail = _event_detail(name, arguments)
@@ -166,18 +222,32 @@ class ConnectionTracker(Middleware):
name=agent_name,
mime_type="text/markdown",
))
for name in state.discover_modules(PROJECT_DIR):
modules = state.discover_modules(PROJECT_DIR)
if modules:
result.append(Resource(
uri=f"agent://{agent_name}/modules/{name}",
name=f"modules / {name}",
uri=f"agent://{agent_name}/modules",
name="modules",
mime_type="text/markdown",
))
for name in state.load_connections(PROJECT_DIR):
for name in modules:
result.append(Resource(
uri=f"agent://{agent_name}/modules/{name}",
name=f"modules / {name}",
mime_type="text/markdown",
))
connections = state.load_connections(PROJECT_DIR)
if connections:
result.append(Resource(
uri=f"agent://{agent_name}/connections/{name}",
name=f"connections / {name}",
uri=f"agent://{agent_name}/connections",
name="connections",
mime_type="text/markdown",
))
for name in connections:
result.append(Resource(
uri=f"agent://{agent_name}/connections/{name}",
name=f"connections / {name}",
mime_type="text/markdown",
))
return result
async def on_read_resource(self, context, call_next):
@@ -210,6 +280,7 @@ async def status_route(request: Request) -> JSONResponse:
"name": config.get("name", PROJECT_DIR.name),
"project_dir": str(PROJECT_DIR.resolve()),
"sessions": list(SESSIONS.values()),
"stale": check_staleness(force=True),
})
+20
View File
@@ -83,6 +83,26 @@ def test_api_tree_excludes_machine_and_secret_files(client):
assert not any(p.startswith(".venv") for p in paths)
def test_status_reports_staleness(client, project):
import os
server.snapshot_startup_files()
stale = client.get("/status").json()["stale"]
assert stale == {"agent_md": False, "commands": False}
agent_md = project / "agent.md"
os.utime(agent_md, (agent_md.stat().st_mtime + 10,) * 2)
stale = client.get("/status").json()["stale"]
assert stale["agent_md"] is True
assert stale["commands"] is False
cmd = project / "modules" / "notes" / "commands" / "report.md"
cmd.parent.mkdir(parents=True)
cmd.write_text("---\ndescription: d\n---\nbody\n")
stale = client.get("/status").json()["stale"]
assert stale["commands"] is True
def test_api_events_limit_since_and_ring_cap(client):
for i in range(350):
server.record_event("s", "tool", f"tool{i}")
+26
View File
@@ -98,6 +98,32 @@ def test_new_file_without_parent_index_does_not_warn(project):
assert "Warning" not in out
def test_agent_md_write_notes_restart(project):
out = server.write_file("agent.md", "# Agent\nbe useful\n")
assert "Note: agent.md is pushed at connect" in out
assert "restart" in out
assert "Warning" not in out # agent.md stays exempt from the index check
def test_command_write_notes_restart(project):
out = server.write_file(
"connections/gmail/commands/send.md", "---\ndescription: d\n---\nbody\n"
)
assert "Note: commands are registered at server start" in out
out = server.write_file(
"modules/notes/commands/report.py", "# ---\n# description: d\n# ---\n"
)
assert "Note: commands are registered at server start" in out
# Not a command file: wrong folder or wrong extension.
assert "Note:" not in server.write_file("modules/notes/scripts/report.py", "x")
assert "Note:" not in server.write_file("modules/notes/commands/notes.txt", "x")
def test_ordinary_write_has_no_restart_note(project):
out = server.write_file("modules/notes/index.md", "summary")
assert "Note:" not in out
def test_write_new_file_reports_size_and_lines(project):
out = server.write_file("modules/notes/note.md", "one\ntwo\n")
assert out.startswith("Created: modules/notes/note.md")