一键导入
hgraf-safe-buffer-edit
Safely edit the client ring buffer and transport worker — preserve drop ordering, thread safety, and reconnect-resume semantics.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Safely edit the client ring buffer and transport worker — preserve drop ordering, thread safety, and reconnect-resume semantics.
用 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-safe-buffer-edit |
| description | Safely edit the client ring buffer and transport worker — preserve drop ordering, thread safety, and reconnect-resume semantics. |
You're modifying client/harmonograf_client/buffer.py or the drain/resume path in client/harmonograf_client/transport.py. These two modules are the client's back-pressure relief valve — a subtle bug here silently drops telemetry or stalls the upstream.
buffer.py:1-34 module docstring — the tiered drop policy is specified there: updates → payload chunks → whole spans. This ordering is a contract with docs/design/01 §4.5, not an implementation detail.buffer.py:45-80 — EnvelopeKind, SpanEnvelope, BufferStats. The buffer is deliberately protobuf-agnostic; it holds opaque payloads.buffer.py EventRingBuffer.push — the push/overflow routine. Look for the _drop_oldest_update → _strip_oldest_payload_ref → _drop_oldest_span fallback chain.buffer.py PayloadBuffer — content-addressed blobs keyed by digest. Oldest-first eviction on byte budget overrun.transport.py drain loop (grep for pop_batch) — the worker that moves envelopes from the buffer onto the gRPC stream. Understand when it acks and when it retains.transport.py:649 _dispatch_control and :700 _control_kind_name — control acks flow back up through StreamTelemetry, so the buffer must not block the control handler.updates → payload refs → whole spans. The reason: updates are lossy (the next update carries cumulative state), payload refs are recoverable (the blob stays in PayloadBuffer until byte pressure), whole spans lose observability. Reversing the order quietly degrades the UI — the Gantt bar still renders but the inside of the span goes blank.
If you need a new envelope kind, decide its eviction tier explicitly and document it in the module docstring. Don't leave the order implicit in push() branches.
push never blocksbuffer.py:26 — "non-blocking: push never waits". The instrumentation callers run inside ADK callbacks on the agent's hot path. A push that blocks on a lock held by the drain worker deadlocks the agent. Use the existing threading.Lock with short critical sections; never await inside push.
BufferStats.dropped_updates / dropped_payload_refs / dropped_spans are reported via Heartbeat.buffered_events and protocol metrics. They must be monotonic: resetting them on reconnect breaks the rate computation on the server side. If you need "since-last-flush" counts, derive them from monotonic readings on the consumer side.
The buffer doesn't enforce this — the caller does — but the drop policy does: a lone SpanUpdate whose parent SpanStart was already drained is valid; a SpanEnd without a preceding SpanStart is a bug on the sender side. If you change eviction to drop SpanStart before its SpanEnd, the server will see an end-without-start and log a protocol violation. Keep the invariant: never drop a SpanStart if its matching SpanEnd is still in the buffer.
When _strip_oldest_payload_ref runs, the envelope's has_payload_ref flag flips to False but the blob in PayloadBuffer remains. The transport layer relies on has_payload_ref to decide whether to upload. If you add a new payload owner, update both the ref flag and the blob eviction atomically.
proto/harmonograf/v1/types.proto:387-390 — control delivery is full-duplex. The transport's stream-handler reads control downstream and writes acks upstream multiplexed with telemetry. If your buffer change makes telemetry the exclusive writer, control acks stall. Rule: never gate control acks on buffer drain progress.
EnvelopeKind (buffer.py:45)._drop_oldest_* methods to treat the new kind correctly — or confirm the existing fallthrough does the right thing.client/tests/test_buffer.py that fills the buffer with only the new kind and asserts eviction order.EventRingBuffer(capacity=2000) and PayloadBuffer(max_bytes=16 * 1024 * 1024) are the defaults. Both are overridable via HarmonografClient(...) kwargs (grep client.py for the ctor forwarding). Before changing the default:
Ship the change alongside a heartbeat metric regression test.
transport.py :: _drain_loop (grep for pop_batch):
await asyncio.* inside it — the transport uses a thread-safe queue bridge to the async grpc stub.pop_batch call must be followed by either a successful stream write or a re-push of the envelope for a retry. Losing an envelope between pop and write without counting it as dropped is the worst kind of bug — silent data loss.When the gRPC stream breaks:
send raises.transport.py catches, backs off, and reconnects.BufferStats.heartbeat.py build_heartbeat(stats) → Heartbeat proto field.hgraf-add-proto-field.md in batch 1).ProtocolMetrics so the CLI format_protocol_metrics shows it.When editing EventRingBuffer:
self._deque or self._stats must be inside self._lock._locked private variants.uv run pytest client/tests/test_buffer.py -x -q
uv run pytest client/tests/test_transport_mock.py -x -q
uv run pytest client/tests -x -q -k "reconnect or resume or overflow"
For load testing:
uv run python -m harmonograf_client.dev.load_test --rate 100 --duration 30
(Grep for a load test script in the client dev tools; if none exists, write one as part of the change.)
drop_session(session_id) method, not on reconnect.stats.dropped_updates read without the lock is technically racy. For metrics it's acceptable (monotonic), but for tests use stats_snapshot() under the lock.has_payload_ref: a span whose payload ref was stripped but has_payload_ref=True causes the transport to request a blob that was evicted. The caller sees a missing-blob server error. Always flip the flag when stripping.pb.SpanStart message, you couple the buffer to the proto module and break the layering. Convert at drain time in transport.py.transport.py that writes directly to the stream. If a refactor consolidates them, control latency becomes a function of buffer depth — an anti-goal.self._lock then calling into the transport within the same critical section deadlocks on reconnect. Drain outside the lock.