agent-memory-brain 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_memory_brain-0.2.0.dist-info/METADATA +98 -0
- agent_memory_brain-0.2.0.dist-info/RECORD +54 -0
- agent_memory_brain-0.2.0.dist-info/WHEEL +4 -0
- agent_memory_brain-0.2.0.dist-info/entry_points.txt +3 -0
- agent_memory_brain-0.2.0.dist-info/licenses/LICENSE +67 -0
- devmem/__init__.py +3 -0
- devmem/ado.py +433 -0
- devmem/agent_colors.py +74 -0
- devmem/cli.py +500 -0
- devmem/db.py +1069 -0
- devmem/dispatch.py +478 -0
- devmem/embed.py +111 -0
- devmem/filelock.py +53 -0
- devmem/git_sync.py +261 -0
- devmem/guard.py +214 -0
- devmem/hook_log.py +49 -0
- devmem/ingest/__init__.py +1 -0
- devmem/ingest/claude.py +87 -0
- devmem/ingest/codex.py +175 -0
- devmem/ingest/runner.py +318 -0
- devmem/ingest/subagents.py +250 -0
- devmem/maintenance.py +801 -0
- devmem/mcp_server.py +542 -0
- devmem/messages.py +684 -0
- devmem/panel/__init__.py +1 -0
- devmem/panel/app.py +971 -0
- devmem/panel/state.py +358 -0
- devmem/panel/static/base.css +27 -0
- devmem/panel/static/brain.css +473 -0
- devmem/panel/static/brain.js +1118 -0
- devmem/panel/static/brain3d.js +600 -0
- devmem/panel/static/chat.js +18 -0
- devmem/panel/static/common.js +208 -0
- devmem/panel/static/site.css +508 -0
- devmem/panel/static/tokens.css +19 -0
- devmem/panel/templates/ado.html +62 -0
- devmem/panel/templates/agents.html +35 -0
- devmem/panel/templates/audit.html +19 -0
- devmem/panel/templates/base.html +42 -0
- devmem/panel/templates/brain.html +101 -0
- devmem/panel/templates/maintenance.html +32 -0
- devmem/panel/templates/memory.html +71 -0
- devmem/panel/templates/message_thread.html +49 -0
- devmem/panel/templates/messages.html +8 -0
- devmem/panel/templates/overview.html +109 -0
- devmem/panel/templates/project.html +48 -0
- devmem/panel/templates/projects.html +21 -0
- devmem/panel/templates/session.html +49 -0
- devmem/panel/templates/sessions.html +67 -0
- devmem/panel/templates/subagent.html +27 -0
- devmem/procs.py +10 -0
- devmem/projects.py +17 -0
- devmem/secrets.py +9 -0
- devmem/skills.py +105 -0
devmem/panel/state.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Live snapshot (/api/state) and pulse (/api/stream) queries.
|
|
2
|
+
|
|
3
|
+
Timestamps in the DB are mixed ISO formats, so time windows always compare with datetime(ts). A plain text
|
|
4
|
+
prefilter on the date (ts >= 'YYYY-MM-DD' a day earlier than needed) lets the ts indexes skip old rows first.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from datetime import UTC, datetime, timedelta
|
|
14
|
+
from pathlib import PurePath
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .. import messages
|
|
18
|
+
from ..db import SCHEMA_VERSION, not_superseded_sql, parse_ts
|
|
19
|
+
|
|
20
|
+
STALE_AFTER_S = 6 * 3600
|
|
21
|
+
RECENT_SESSION_S = 600
|
|
22
|
+
CONNECTED_S = 300
|
|
23
|
+
PULSE_MAX_AGE_S = 600
|
|
24
|
+
MAX_PROJECTS = 12
|
|
25
|
+
MAX_MEMORIES = 250
|
|
26
|
+
FEED_LEN = 30
|
|
27
|
+
TOKEN_COLS = ("input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _now() -> datetime:
|
|
31
|
+
return datetime.now(UTC)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _prefilter(days: float) -> str:
|
|
35
|
+
"""Date-only lower bound, one day wider than the real window, valid for every ISO variant."""
|
|
36
|
+
return (_now() - timedelta(days=days + 1)).strftime("%Y-%m-%d")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rows(conn, sql: str, params=()) -> list[dict[str, Any]]:
|
|
40
|
+
return [dict(r) for r in conn.execute(sql, params).fetchall()]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _iso(value) -> str | None:
|
|
44
|
+
parsed = parse_ts(value)
|
|
45
|
+
return parsed.isoformat().replace("+00:00", "Z") if parsed else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def basename(project: str | None) -> str:
|
|
49
|
+
return PurePath(project).name or project if project else "global"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------- limits
|
|
53
|
+
|
|
54
|
+
def limits(conn) -> list[dict[str, Any]]:
|
|
55
|
+
rows = _rows(conn, """SELECT * FROM (
|
|
56
|
+
SELECT r.*, ROW_NUMBER() OVER (PARTITION BY agent,label ORDER BY datetime(ts) DESC, id DESC) rn
|
|
57
|
+
FROM rate_limits r WHERE label IS NOT NULL) WHERE rn=1""")
|
|
58
|
+
now = _now()
|
|
59
|
+
out = []
|
|
60
|
+
for r in rows:
|
|
61
|
+
used = float(r["used_percent"]) if r["used_percent"] is not None else None
|
|
62
|
+
resets = parse_ts(r["resets_at"])
|
|
63
|
+
as_of = max(filter(None, (parse_ts(r["ts"]), parse_ts(r.get("observed_at")))), default=None)
|
|
64
|
+
age = (now - as_of).total_seconds() if as_of else None
|
|
65
|
+
out.append({
|
|
66
|
+
"agent": r["agent"], "label": r["label"], "window_minutes": r["window_minutes"],
|
|
67
|
+
"used_percent": used, "left_percent": None if used is None else max(0.0, round(100 - used, 1)),
|
|
68
|
+
"resets_at": _iso(resets), "resets_in_s": int((resets - now).total_seconds()) if resets else None,
|
|
69
|
+
"reset_passed": bool(resets and resets <= now),
|
|
70
|
+
"as_of": _iso(as_of), "age_s": int(age) if age is not None else None,
|
|
71
|
+
"stale": age is None or age > STALE_AFTER_S,
|
|
72
|
+
})
|
|
73
|
+
order = {"claude": 0, "codex": 1}
|
|
74
|
+
out.sort(key=lambda x: (order.get(x["agent"], 9), x["window_minutes"] or 99999, x["label"]))
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------- tokens
|
|
79
|
+
|
|
80
|
+
def _token_dict(row: dict[str, Any], prefix: str = "") -> dict[str, int]:
|
|
81
|
+
d = {
|
|
82
|
+
"input": int(row.get(prefix + "input") or 0), "output": int(row.get(prefix + "output") or 0),
|
|
83
|
+
"cache_read": int(row.get(prefix + "cache_read") or 0), "cache_write": int(row.get(prefix + "cache_write") or 0),
|
|
84
|
+
}
|
|
85
|
+
d["total"] = sum(d.values())
|
|
86
|
+
return d
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tokens(conn) -> dict[str, Any]:
|
|
90
|
+
today_start = conn.execute("SELECT datetime('now','localtime','start of day','utc')").fetchone()[0]
|
|
91
|
+
pre = _prefilter(7)
|
|
92
|
+
sums = lambda cond, p: ", ".join(f"SUM(CASE WHEN {cond} THEN {c} ELSE 0 END) {p}{n}" for c, n in zip(TOKEN_COLS, ("input", "output", "cache_read", "cache_write")))
|
|
93
|
+
per_agent = _rows(conn, f"""SELECT agent, {sums('datetime(ts) >= :today', 't_')}, {sums("datetime(ts) >= datetime('now','-7 days')", 'w_')}
|
|
94
|
+
FROM usage_events WHERE ts >= :pre GROUP BY agent""", {"today": today_start, "pre": pre})
|
|
95
|
+
models = _rows(conn, f"""SELECT agent, model, {sums('1', '')} FROM usage_events
|
|
96
|
+
WHERE ts >= :pre AND datetime(ts) >= :today GROUP BY agent, model""", {"today": today_start, "pre": _prefilter(1)})
|
|
97
|
+
hourly = _rows(conn, """SELECT agent, strftime('%Y-%m-%dT%H', datetime(ts)) hour,
|
|
98
|
+
SUM(input_tokens+output_tokens+cache_read_tokens+cache_write_tokens) total
|
|
99
|
+
FROM usage_events WHERE ts >= ? AND datetime(ts) >= datetime('now','-24 hours') GROUP BY agent, hour""", (_prefilter(1),))
|
|
100
|
+
now = _now().replace(minute=0, second=0, microsecond=0)
|
|
101
|
+
hours = [(now - timedelta(hours=23 - i)).strftime("%Y-%m-%dT%H") for i in range(24)]
|
|
102
|
+
spark: dict[str, list[int]] = {}
|
|
103
|
+
for h in hourly:
|
|
104
|
+
if h["hour"] in hours:
|
|
105
|
+
spark.setdefault(h["agent"], [0] * 24)[hours.index(h["hour"])] = int(h["total"] or 0)
|
|
106
|
+
today = {r["agent"]: _token_dict(r, "t_") for r in per_agent}
|
|
107
|
+
week = {r["agent"]: _token_dict(r, "w_") for r in per_agent}
|
|
108
|
+
model_rows = [{"agent": m["agent"], "model": m["model"], **_token_dict(m)} for m in models]
|
|
109
|
+
model_rows.sort(key=lambda m: -m["total"])
|
|
110
|
+
return {
|
|
111
|
+
"today_start": _iso(today_start), "today": today, "week": week,
|
|
112
|
+
"today_total": sum(v["total"] for v in today.values()), "week_total": sum(v["total"] for v in week.values()),
|
|
113
|
+
"models_today": model_rows, "hourly": {"hours": [h + ":00:00Z" for h in hours], "series": spark},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------- agents and sessions
|
|
118
|
+
|
|
119
|
+
def _pid_alive(pid) -> bool:
|
|
120
|
+
try:
|
|
121
|
+
os.kill(int(pid), 0)
|
|
122
|
+
return True
|
|
123
|
+
except (OSError, TypeError, ValueError):
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def client_connected(client: dict[str, Any]) -> bool:
|
|
128
|
+
"""Same rule as the /agents page: not ended, seen in the last 5 minutes and PID alive."""
|
|
129
|
+
seen = parse_ts(client.get("last_seen_at"))
|
|
130
|
+
return (client.get("ended_at") is None and seen is not None
|
|
131
|
+
and (_now() - seen).total_seconds() < CONNECTED_S and _pid_alive(client.get("pid")))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def agent_family(name: str | None) -> str:
|
|
135
|
+
name = (name or "").lower()
|
|
136
|
+
if "codex" in name:
|
|
137
|
+
return "codex"
|
|
138
|
+
if "claude" in name:
|
|
139
|
+
return "claude"
|
|
140
|
+
return name or "mcp"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def connected_clients(conn) -> list[dict[str, Any]]:
|
|
144
|
+
rows = _rows(conn, """SELECT a.id, a.client_name, a.client_version, a.pid, a.project, a.started_at, a.last_seen_at, a.ended_at,
|
|
145
|
+
(SELECT tool FROM tool_calls t WHERE t.client_id=a.id ORDER BY t.id DESC LIMIT 1) last_tool
|
|
146
|
+
FROM agent_clients a WHERE a.ended_at IS NULL ORDER BY a.id DESC LIMIT 100""")
|
|
147
|
+
out = []
|
|
148
|
+
for r in rows:
|
|
149
|
+
if client_connected(r):
|
|
150
|
+
out.append({"id": r["id"], "client_name": r["client_name"], "family": agent_family(r["client_name"]), "pid": r["pid"],
|
|
151
|
+
"project": r["project"], "project_name": basename(r["project"]), "last_seen": _iso(r["last_seen_at"]),
|
|
152
|
+
"last_tool": r["last_tool"]})
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def recent_sessions(conn) -> list[dict[str, Any]]:
|
|
157
|
+
pre = _prefilter(1)
|
|
158
|
+
window = f"-{RECENT_SESSION_S} seconds"
|
|
159
|
+
acts = _rows(conn, """SELECT agent, session_id, project, ts, name, summary, kind FROM activity_events
|
|
160
|
+
WHERE ts >= ? AND datetime(ts) >= datetime('now', ?) ORDER BY datetime(ts) DESC, id DESC LIMIT 2000""", (pre, window))
|
|
161
|
+
usage = _rows(conn, """SELECT agent, session_id, project, MAX(datetime(ts)) ts,
|
|
162
|
+
SUM(input_tokens+output_tokens+cache_read_tokens+cache_write_tokens) tokens, MAX(model) model
|
|
163
|
+
FROM usage_events WHERE ts >= ? AND datetime(ts) >= datetime('now', ?) GROUP BY agent, session_id, project""", (pre, window))
|
|
164
|
+
sessions: dict[tuple, dict[str, Any]] = {}
|
|
165
|
+
for a in acts: # newest first: the first seen row per session is its last action
|
|
166
|
+
key = (a["agent"], a["session_id"])
|
|
167
|
+
s = sessions.setdefault(key, {"agent": a["agent"], "session_id": a["session_id"], "project": a["project"],
|
|
168
|
+
"last_ts": a["ts"], "last_tool": a["name"] if a["kind"] == "tool" else None,
|
|
169
|
+
"last_summary": a["summary"] if a["kind"] == "tool" else "", "tools_10m": 0, "tokens_10m": 0, "model": None})
|
|
170
|
+
s["tools_10m"] += a["kind"] == "tool"
|
|
171
|
+
if s["last_tool"] is None and a["kind"] == "tool":
|
|
172
|
+
s["last_tool"], s["last_summary"] = a["name"], a["summary"]
|
|
173
|
+
for u in usage:
|
|
174
|
+
# Subagent tokens are logged under "claude"; their activity under "claude-subagent".
|
|
175
|
+
key = (u["agent"], u["session_id"])
|
|
176
|
+
s = sessions.setdefault(key, {"agent": u["agent"], "session_id": u["session_id"], "project": u["project"], "last_ts": u["ts"],
|
|
177
|
+
"last_tool": None, "last_summary": "", "tools_10m": 0, "tokens_10m": 0, "model": None})
|
|
178
|
+
s["tokens_10m"] += int(u["tokens"] or 0)
|
|
179
|
+
s["model"] = u["model"]
|
|
180
|
+
if (parse_ts(u["ts"]) or _now()) > (parse_ts(s["last_ts"]) or _now()):
|
|
181
|
+
s["last_ts"] = u["ts"]
|
|
182
|
+
s["project"] = s["project"] or u["project"]
|
|
183
|
+
for a in _rows(conn, """SELECT s.agent_id,s.project,s.last_activity_at,s.model,
|
|
184
|
+
(SELECT name FROM subagent_tools t WHERE t.agent_id=s.agent_id ORDER BY source_offset DESC LIMIT 1) last_tool,
|
|
185
|
+
(SELECT arg FROM subagent_tools t WHERE t.agent_id=s.agent_id ORDER BY source_offset DESC LIMIT 1) last_summary
|
|
186
|
+
FROM subagents s WHERE s.agent_kind='codex' AND datetime(COALESCE(s.last_activity_at,s.started_at)) >= datetime('now', ?)""", (window,)):
|
|
187
|
+
key = ("codex", a["agent_id"])
|
|
188
|
+
s = sessions.setdefault(key, {"agent": "codex", "session_id": a["agent_id"], "project": a["project"], "last_ts": a["last_activity_at"],
|
|
189
|
+
"last_tool": a["last_tool"], "last_summary": a["last_summary"] or "", "tools_10m": 0, "tokens_10m": 0, "model": a["model"]})
|
|
190
|
+
s["last_tool"], s["last_summary"], s["model"] = a["last_tool"] or s["last_tool"], a["last_summary"] or s["last_summary"], a["model"] or s["model"]
|
|
191
|
+
now = _now()
|
|
192
|
+
out = []
|
|
193
|
+
for s in sessions.values():
|
|
194
|
+
last = parse_ts(s["last_ts"])
|
|
195
|
+
s["last_ts"] = _iso(last)
|
|
196
|
+
s["age_s"] = int((now - last).total_seconds()) if last else None
|
|
197
|
+
s["project_name"] = basename(s["project"])
|
|
198
|
+
out.append(s)
|
|
199
|
+
out.sort(key=lambda s: s["age_s"] if s["age_s"] is not None else 1e9)
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------- activity feed
|
|
204
|
+
|
|
205
|
+
def activity(conn, limit: int = FEED_LEN) -> list[dict[str, Any]]:
|
|
206
|
+
rows = _rows(conn, """SELECT id, agent, session_id, project, ts, kind, name, summary FROM activity_events
|
|
207
|
+
WHERE ts >= ? ORDER BY datetime(ts) DESC, id DESC LIMIT ?""", (_prefilter(7), limit))
|
|
208
|
+
if len(rows) < limit:
|
|
209
|
+
rows = _rows(conn, "SELECT id, agent, session_id, project, ts, kind, name, summary FROM activity_events "
|
|
210
|
+
"ORDER BY datetime(ts) DESC, id DESC LIMIT ?", (limit,))
|
|
211
|
+
for r in rows:
|
|
212
|
+
r["ts"] = _iso(r["ts"])
|
|
213
|
+
r["project_name"] = basename(r["project"])
|
|
214
|
+
return rows
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------- brain graph
|
|
218
|
+
|
|
219
|
+
def graph(conn, sessions: list[dict[str, Any]], clients: list[dict[str, Any]]) -> dict[str, Any]:
|
|
220
|
+
pre = _prefilter(7)
|
|
221
|
+
last: dict[str, datetime] = {}
|
|
222
|
+
counts: dict[str, dict[str, int]] = {}
|
|
223
|
+
|
|
224
|
+
def bump(project, ts, field, n):
|
|
225
|
+
if not project:
|
|
226
|
+
return
|
|
227
|
+
t = parse_ts(ts)
|
|
228
|
+
if t and (project not in last or t > last[project]):
|
|
229
|
+
last[project] = t
|
|
230
|
+
c = counts.setdefault(project, {"activity": 0, "memories": 0})
|
|
231
|
+
c[field] += int(n or 0)
|
|
232
|
+
|
|
233
|
+
for r in _rows(conn, "SELECT project, MAX(datetime(ts)) ts, COUNT(*) n FROM activity_events WHERE ts >= ? AND datetime(ts) >= datetime('now','-7 days') GROUP BY project", (pre,)):
|
|
234
|
+
bump(r["project"], r["ts"], "activity", r["n"])
|
|
235
|
+
for r in _rows(conn, "SELECT project, MAX(ts) ts FROM usage_events GROUP BY project"): # served by usage_project_ts
|
|
236
|
+
bump(r["project"], r["ts"], "activity", 0)
|
|
237
|
+
for r in _rows(conn, "SELECT project, COUNT(*) n FROM usage_events WHERE ts >= ? AND datetime(ts) >= datetime('now','-7 days') GROUP BY project", (pre,)):
|
|
238
|
+
bump(r["project"], None, "activity", r["n"])
|
|
239
|
+
for r in _rows(conn, "SELECT project, MAX(updated_at) ts, COUNT(*) n FROM memories WHERE project IS NOT NULL GROUP BY project"):
|
|
240
|
+
bump(r["project"], r["ts"], "memories", r["n"])
|
|
241
|
+
for s in sessions:
|
|
242
|
+
bump(s["project"], s["last_ts"], "activity", 0)
|
|
243
|
+
ranked = sorted(last, key=lambda p: last[p], reverse=True)[:MAX_PROJECTS]
|
|
244
|
+
chosen = set(ranked)
|
|
245
|
+
mems = _rows(conn, "SELECT id, title, kind, project, created_at FROM memories ORDER BY id DESC LIMIT ?", (MAX_MEMORIES,))
|
|
246
|
+
other = False
|
|
247
|
+
for m in mems:
|
|
248
|
+
m["title"] = (m["title"] or "")[:80]
|
|
249
|
+
if m["project"] is None:
|
|
250
|
+
m["cluster"] = "global"
|
|
251
|
+
elif m["project"] in chosen:
|
|
252
|
+
m["cluster"] = m["project"]
|
|
253
|
+
else:
|
|
254
|
+
m["cluster"], other = "other", True
|
|
255
|
+
m["created_at"] = _iso(m["created_at"])
|
|
256
|
+
memory_ids = {m["id"] for m in mems}
|
|
257
|
+
links = [dict(r) for r in conn.execute("SELECT from_id,to_id,type FROM memory_links ORDER BY type,from_id,to_id").fetchall()
|
|
258
|
+
if r["from_id"] in memory_ids and r["to_id"] in memory_ids]
|
|
259
|
+
projects = [{"id": p, "name": basename(p), "path": p, "last_activity": _iso(last[p]), **counts.get(p, {"activity": 0, "memories": 0})}
|
|
260
|
+
for p in ranked]
|
|
261
|
+
if other:
|
|
262
|
+
projects.append({"id": "other", "name": "other", "path": None, "last_activity": None, "activity": 0,
|
|
263
|
+
"memories": sum(1 for m in mems if m["cluster"] == "other")})
|
|
264
|
+
agents = []
|
|
265
|
+
for s in sessions[:10]:
|
|
266
|
+
agents.append({"id": f"{s['agent']}:{s['session_id']}", "agent": s["agent"], "session_id": s["session_id"],
|
|
267
|
+
"project": s["project"] if s["project"] in chosen else None, "project_name": s["project_name"],
|
|
268
|
+
"active": s["age_s"] is not None and s["age_s"] < 120, "connected": False})
|
|
269
|
+
covered = {(agent_family(a["agent"]), a["project"]) for a in agents}
|
|
270
|
+
for c in clients:
|
|
271
|
+
if (c["family"], c["project"]) in covered:
|
|
272
|
+
for a in agents:
|
|
273
|
+
if agent_family(a["agent"]) == c["family"] and a["project"] == c["project"]:
|
|
274
|
+
a["connected"] = True
|
|
275
|
+
continue
|
|
276
|
+
covered.add((c["family"], c["project"]))
|
|
277
|
+
agents.append({"id": f"mcp:{c['id']}", "agent": c["family"], "session_id": None,
|
|
278
|
+
"project": c["project"] if c["project"] in chosen else None, "project_name": c["project_name"],
|
|
279
|
+
"active": False, "connected": True})
|
|
280
|
+
return {"projects": projects, "memories": mems, "agents": agents, "links": links}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def graph_signature(g: dict[str, Any]) -> str:
|
|
284
|
+
"""Changes when nodes or edges change (not on per-project counters), so clients re-layout only when needed."""
|
|
285
|
+
key = [[p["id"] for p in g["projects"]], [(m["id"], m["cluster"], m["kind"]) for m in g["memories"]],
|
|
286
|
+
[(a["id"], a["project"], a["active"], a["connected"]) for a in g["agents"]],
|
|
287
|
+
[(l["from_id"], l["to_id"], l["type"]) for l in g.get("links", [])]]
|
|
288
|
+
return hashlib.sha1(json.dumps(key, default=str).encode()).hexdigest()[:16]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------- snapshot and pulses
|
|
292
|
+
|
|
293
|
+
def memory_counts(conn) -> dict[str, Any]:
|
|
294
|
+
by_kind = {r["kind"]: r["n"] for r in _rows(conn, "SELECT kind, COUNT(*) n FROM memories GROUP BY kind")}
|
|
295
|
+
return {"memories_by_kind": by_kind, "memories_total": sum(by_kind.values())}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def reminders(conn) -> list[dict[str, Any]]:
|
|
299
|
+
"""Open reminder-tagged memories, newest first, for the Brain panel."""
|
|
300
|
+
rows = _rows(conn, f"""SELECT id,title,content,created_at,tags FROM memories m
|
|
301
|
+
WHERE {not_superseded_sql()} ORDER BY datetime(created_at) DESC, id DESC""")
|
|
302
|
+
return [{"id": row["id"], "title": row["title"] or row["content"][:80], "created_at": _iso(row["created_at"])}
|
|
303
|
+
for row in rows if "remind" in re.split(r"[\s,]+", (row["tags"] or "").lower())]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cursor(conn) -> dict[str, int]:
|
|
307
|
+
one = lambda t: conn.execute(f"SELECT COALESCE(MAX(id),0) FROM {t}").fetchone()[0]
|
|
308
|
+
return {"activity": one("activity_events"), "usage": one("usage_events"), "memory": one("memories")}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def build_state(conn) -> dict[str, Any]:
|
|
312
|
+
started = time.monotonic()
|
|
313
|
+
clients = connected_clients(conn)
|
|
314
|
+
sessions = recent_sessions(conn)
|
|
315
|
+
g = graph(conn, sessions, clients)
|
|
316
|
+
state = {
|
|
317
|
+
"now": _iso(_now()), "schema_version": SCHEMA_VERSION,
|
|
318
|
+
"limits": limits(conn), "tokens": tokens(conn),
|
|
319
|
+
"agents": {"connected": clients, "sessions": sessions,
|
|
320
|
+
"subagents": messages.session_subagents(conn, [s["session_id"] for s in sessions if s["agent"].startswith("claude")])},
|
|
321
|
+
"activity": activity(conn), "graph": g, "graph_sig": graph_signature(g),
|
|
322
|
+
"counts": memory_counts(conn), "reminders": reminders(conn), "cursor": cursor(conn),
|
|
323
|
+
}
|
|
324
|
+
state["build_ms"] = int((time.monotonic() - started) * 1000)
|
|
325
|
+
return state
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def pulses_since(conn, since: dict[str, int]) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
|
329
|
+
"""Discrete events newer than the cursor (by id) and no older than 10 minutes, for front-end animation.
|
|
330
|
+
|
|
331
|
+
The age filter keeps a first import of historical logs from replaying as a storm of pulses.
|
|
332
|
+
"""
|
|
333
|
+
window = f"-{PULSE_MAX_AGE_S} seconds"
|
|
334
|
+
out: list[dict[str, Any]] = []
|
|
335
|
+
new = dict(since)
|
|
336
|
+
acts = _rows(conn, """SELECT id, agent, session_id, project, ts, kind, name, summary FROM activity_events
|
|
337
|
+
WHERE id > ? ORDER BY id LIMIT 500""", (since.get("activity", 0),))
|
|
338
|
+
if acts:
|
|
339
|
+
new["activity"] = acts[-1]["id"]
|
|
340
|
+
now = _now()
|
|
341
|
+
for a in acts:
|
|
342
|
+
t = parse_ts(a["ts"])
|
|
343
|
+
if t and (now - t).total_seconds() <= PULSE_MAX_AGE_S:
|
|
344
|
+
out.append({"type": a["kind"], "agent": a["agent"], "session_id": a["session_id"], "project": a["project"],
|
|
345
|
+
"project_name": basename(a["project"]), "name": a["name"], "summary": a["summary"], "ts": _iso(t), "id": a["id"]})
|
|
346
|
+
usage = _rows(conn, """SELECT MAX(id) id, agent, session_id, project, MAX(ts) ts, SUM(input_tokens+output_tokens+cache_read_tokens+cache_write_tokens) tokens
|
|
347
|
+
FROM usage_events WHERE id > ? AND datetime(ts) >= datetime('now', ?) GROUP BY agent, session_id, project""", (since.get("usage", 0), window))
|
|
348
|
+
new["usage"] = max(new.get("usage", 0), conn.execute("SELECT COALESCE(MAX(id),0) FROM usage_events").fetchone()[0])
|
|
349
|
+
for u in usage:
|
|
350
|
+
out.append({"type": "usage", "agent": u["agent"], "session_id": u["session_id"], "project": u["project"],
|
|
351
|
+
"project_name": basename(u["project"]), "tokens": int(u["tokens"] or 0), "ts": _iso(u["ts"])})
|
|
352
|
+
mems = _rows(conn, "SELECT id, title, kind, project, created_at, source_agent FROM memories WHERE id > ? ORDER BY id LIMIT 100", (since.get("memory", 0),))
|
|
353
|
+
if mems:
|
|
354
|
+
new["memory"] = mems[-1]["id"]
|
|
355
|
+
for m in mems:
|
|
356
|
+
out.append({"type": "memory", "id": m["id"], "title": (m["title"] or "")[:80], "kind": m["kind"], "project": m["project"],
|
|
357
|
+
"project_name": basename(m["project"]), "agent": m["source_agent"], "ts": _iso(m["created_at"])})
|
|
358
|
+
return out[-80:], new
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
2
|
+
html, body { overflow-x: clip; }
|
|
3
|
+
html { scrollbar-color: var(--color-muted) var(--color-paper); scroll-padding-top: var(--space-3xl); }
|
|
4
|
+
body { margin: 0; background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.55; -webkit-font-smoothing: antialiased; caret-color: var(--color-accent); }
|
|
5
|
+
::selection { background: color-mix(in oklab, var(--color-accent) 35%, transparent); color: var(--color-ink); }
|
|
6
|
+
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
|
7
|
+
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.16em; }
|
|
8
|
+
h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight, 700); font-synthesis: none; letter-spacing: var(--tracking-display); line-height: 1.05; overflow-wrap: break-word; min-width: 0; text-wrap: balance; }
|
|
9
|
+
h1 { max-width: 20ch; font-size: var(--text-display); } h2 { max-width: 30ch; font-size: var(--text-section); } h3 { max-width: 40ch; font-size: var(--text-title); line-height: 1.2; }
|
|
10
|
+
.lead { font-size: var(--text-lead); line-height: 1.45; max-width: 52ch; }
|
|
11
|
+
p { margin: 0; text-wrap: pretty; }
|
|
12
|
+
p:not([class]) { max-width: 65ch; }
|
|
13
|
+
img, video { max-width: 100%; height: auto; display: block; }
|
|
14
|
+
svg { display: block; }
|
|
15
|
+
[hidden] { display: none !important; }
|
|
16
|
+
button, input, select, textarea { font: inherit; }
|
|
17
|
+
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); box-sizing: border-box; }
|
|
18
|
+
.num, .figure, td.num { font-variant-numeric: tabular-nums; }
|
|
19
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
20
|
+
[data-slot] { position: relative; overflow: hidden; background: linear-gradient(160deg, var(--color-paper-2, var(--color-paper)), var(--color-paper-3, var(--color-paper))); }
|
|
21
|
+
[data-slot] > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; }
|
|
22
|
+
[data-slot]:has(> img) > :not(img):not([data-keep]) { display: none !important; }
|
|
23
|
+
@media (max-width: 60rem) { :root { --gutter: 24px; --section-gap-s: 80px; --section-gap: 96px; --section-gap-l: 120px; } }
|
|
24
|
+
@media (max-width: 40rem) { :root { --gutter: 16px; --section-gap-s: 56px; --section-gap: 72px; --section-gap-l: 96px; --gap-columns: 16px; } }
|
|
25
|
+
@media (prefers-reduced-motion: reduce) { *:not(.keep-motion), *:not(.keep-motion)::before, *:not(.keep-motion)::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }
|
|
26
|
+
/* photographs: natural sat=-20 con=10 */
|
|
27
|
+
[data-slot] > img { filter: saturate(0.8) contrast(1.1); }
|