一键导入
hgraf-add-storage-backend
Add a new persistence backend behind the Store ABC — subclass, factory wiring, conformance tests, cascade deletes, and benchmarking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a new persistence backend behind the Store ABC — subclass, factory wiring, conformance tests, cascade deletes, and benchmarking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Understand and safely change heartbeat interval, stuck threshold, and timeout constants — client cadence, server detection math, frontend signal.
Add a new AnnotationKind — proto, storage, ingress RPC, optional downstream delivery to agents, frontend authoring UI, tests.
Add a new agent Capability — proto enum, client advertisement, server ingest, frontend gating in the control UI.
Add a new ControlKind — capability advertisement, client handler, server routing, ack semantics, frontend button, and tests — all the way through.
Add a new drift kind end-to-end. Goldfive owns the detector + enum; harmonograf reflects the kind through the intervention aggregator and the UI timeline.
Script a complex multi-turn FakeLlm scenario — function calls, tool chains, drift events, side-effect hooks, partial responses — for deterministic adk test coverage.
| name | hgraf-add-storage-backend |
| description | Add a new persistence backend behind the Store ABC — subclass, factory wiring, conformance tests, cascade deletes, and benchmarking. |
The two in-tree backends — InMemoryStore (tests, demos) and
SqliteStore (default, single-process) — don't fit a deployment need:
multiple writer processes, horizontal read scaling, retention pushed
into the database, or a substrate the team already operates (Postgres,
DuckDB, ClickHouse). Don't add a backend just to swap one engine for
another — add one when SQLite's single-process / single-writer model is
the actual limiter.
docs/dev-guide/storage-backends.md end to end. It is the
canonical contract — every method, every idempotency requirement,
every bus delta the ingest layer expects.server/harmonograf_server/storage/base.py for the Store
ABC and the dataclasses your backend will round-trip.server/harmonograf_server/storage/sqlite.py as the reference
implementation. It has the same method ordering as the ABC and shows
the schema, column backfill pattern, payload-on-disk layout, and
transactional task-plan upsert.server/harmonograf_server/storage/postgres.py — the stub you
will copy.cp server/harmonograf_server/storage/postgres.py \
server/harmonograf_server/storage/mybackend.py
Rename the class (PostgresStore → MyBackendStore) and update the
module docstring.
Store has ~25 abstract methods. The conformance suite exercises all
of them; if your subclass leaves one unimplemented, instantiation will
fail with TypeError: Can't instantiate abstract class.
Order of work that minimizes thrash:
start / close / ping. Open the connection, run
migrations (or executescript an inline schema), enable any
pragmas you need. close() must tolerate double-close.create_session is idempotent — pick a primary key
and use INSERT ... ON CONFLICT DO NOTHING (or equivalent).
update_session merges metadata, never replaces it.
delete_session is the cascade hub — see step 4.(session_id, agent_id). update_agent_status
is a no-op (not an error) when the agent is missing.append_span is idempotent on span.id. update_span
merges attributes. get_spans time-window query must include
open-ended (running) spans whose end_time is NULL —
COALESCE(end_time, start_time) >= ? is the SQLite trick.gc_payloads removes anything no span references.put_task_plan replaces the task list
wholesale — re-emitted plans must drop tasks the planner removed.
Wrap the plan + tasks delete/insert in a transaction.list_context_window_samples(limit_per_agent=...): the cap is per
agent, not global. Postgres can use a ROW_NUMBER() OVER (PARTITION BY agent_id ...) window; SQLite scans grouped per
agent. Pick whichever is idiomatic.stats. Counts plus disk usage. Memory backend reports 0 for
disk; SQLite walks the payload directory.server/harmonograf_server/storage/factory.py:
from harmonograf_server.storage.mybackend import MyBackendStore
StoreKind = Literal["memory", "sqlite", "postgres", "mybackend"]
def make_store(kind: StoreKind, **opts: Any) -> Store:
...
if kind == "mybackend":
return MyBackendStore(dsn=opts["dsn"], **opts)
...
Then thread the new kind through ServerConfig.store_backend (the
existing literal type lives in server/harmonograf_server/config.py)
and Harmonograf.from_config in main.py if your backend needs custom
kwargs at startup time.
delete_session(session_id) must wipe every related table:
The conformance test test_delete_session_cascades checks all of these
in one call. Forgetting one table is a silent data leak.
If your substrate supports ON DELETE CASCADE, declare it in the
schema and let the database do the work. Otherwise issue the deletes
explicitly inside one transaction.
The ingest pipeline calls create_session, register_agent,
append_span, put_payload, put_task_plan, and put_annotation on
the hot path and has its own dedup, but a network reconnect or a
client retry can still deliver the same message twice. Every insert
must tolerate duplicates without raising. Use primary keys + ON CONFLICT DO NOTHING (or equivalent), not Python-level if exists checks
— the latter race under concurrent writers.
uv run --extra e2e --with pytest --with pytest-asyncio \
python -m pytest tests/storage_conformance_test.py -q
To get your backend exercised by the suite, add one entry to
BACKENDS at the top of tests/storage_conformance_test.py:
BACKENDS: list[BackendSpec] = [
BackendSpec(name="memory", build=lambda _tmp: make_store("memory")),
BackendSpec(name="sqlite", build=lambda tmp: make_store("sqlite", db_path=str(tmp / "h.db"))),
BackendSpec(name="mybackend", build=lambda tmp: make_store("mybackend", dsn=...)),
]
That is the only test-side change. Every conformance test will pick up your new backend automatically. Iterate until all of them pass.
If your backend needs an external service (a real Postgres) the
parametrization should skip it gracefully when the service isn't
available — wrap the build lambda in a check or use
pytest.skip(...) from the fixture.
Do not publish on the SessionBus from inside your backend. The
ingest pipeline (server/harmonograf_server/ingest.py) is the one and
only publisher. The mapping is in
docs/dev-guide/storage-backends.md — if your backend tries to
publish too, you'll get duplicate deltas in the frontend and the
retention sweeper will spam watchers.
For any backend with persistent state, ship versioned migrations
before the first production deploy. SQLite gets away with inline
PRAGMA table_info + ALTER TABLE checks in start() because there
is exactly one writer; that pattern doesn't scale to multi-writer
backends. Use alembic, sqitch, or whatever your team already runs.
Run the existing demo against your backend to catch perf cliffs that the conformance suite (which uses tiny datasets) won't:
HGRAF_STORE_BACKEND=mybackend make demo
Watch for:
get_spans (the SQLite backend has a known one and
it's still fine — but if your DB has higher per-query overhead, fold
the link/payload joins into one query).stats() is hit on every /metricsz scrape.uv run --extra e2e --with pytest --with pytest-asyncio \
python -m pytest tests/storage_conformance_test.py server/tests/ -q
make server-test
All three should be green before you open a PR.
test_delete_session_cascades in
the conformance suite is the single test most likely to catch a
half-finished new backend. Run it early and often.update_span(attributes=...) and
update_session(metadata=...) merge, never replace. A backend that
overwrites silently corrupts running sessions.list_context_window_samples is the only
query whose shape tempts you to reach for window functions. SQLite
ducks them with one query per agent and is plenty fast — don't add
Postgres-isms unless your substrate is Postgres.put_payload. Two clients uploading the
same payload concurrently is normal. The second put_payload must
be a no-op, not a unique-constraint violation.close() is for
process shutdown only. Backends with connection pools should let
the pool live for the process lifetime.