docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering authentication surfaces, outbound-call safety invariants, and other project conventions to guide Claude's understanding of critical system behaviors.
This commit is contained in:
55
.claude/rules/concurrency-threads.md
Normal file
55
.claude/rules/concurrency-threads.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: the Sippy/PJSUA2 OS-thread boundary — two funnels, who owns what state, never cross it directly
|
||||
paths:
|
||||
- "core/sippy_engine.py"
|
||||
- "core/sip_engine.py"
|
||||
- "core/media_pipeline.py"
|
||||
- "core/call_manager.py"
|
||||
- "core/gateway.py"
|
||||
---
|
||||
|
||||
# The thread boundary (the invariant that keeps this app sane)
|
||||
|
||||
The README says "single-process async." That's true at the surface, but under
|
||||
the SIP engine there are **two execution contexts**: the asyncio event loop, and
|
||||
a dedicated **Sippy/PJSUA2 OS thread** running the `ED2` event dispatcher. Almost
|
||||
every hard-to-debug class of bug in a telephony gateway comes from touching one
|
||||
context's state from the other. This app avoids that with exactly one funnel each
|
||||
way. Preserve them.
|
||||
|
||||
- **Who owns what:**
|
||||
- *Sippy thread* owns the Sippy UA objects and the `ED2` dispatcher. State:
|
||||
`_ed_ua_to_leg`, `_ed_leg_to_ua` (the "ED-thread-owned state" maps). Only
|
||||
touch these from a Sippy handler or a `_run_on_sippy` closure.
|
||||
- *asyncio loop* owns everything else: `_legs`, `_bridges`,
|
||||
`_registered_devices`, the `EventBus`, the `CallManager`, the media pipeline
|
||||
wiring.
|
||||
|
||||
- **Cross thread → loop only via `_post_from_ed`.** It calls
|
||||
`asyncio.run_coroutine_threadsafe(self._on_engine_event(kind, data), self._loop)`.
|
||||
`_on_engine_event` is **the single funnel** where Sippy-thread events mutate
|
||||
loop-owned state, and it runs on the loop. New Sippy-side events post through
|
||||
here with a new `kind`; they do **not** reach into `_legs`/`EventBus` directly
|
||||
from the handler.
|
||||
|
||||
- **Cross loop → thread only via `_run_on_sippy`.** It uses `ED2.callFromThread(fn)`
|
||||
so `fn` runs where the Sippy objects live. In simulation mode (no `sippy`
|
||||
import) it runs `fn` inline — keep that fallback so tests and stub mode work
|
||||
without the native library. Anything that manipulates a UA object goes through
|
||||
here.
|
||||
|
||||
- **Never:** read/write a Sippy UA object from the loop; never mutate `_legs`,
|
||||
publish an event, or touch the `CallManager` from inside a raw Sippy callback
|
||||
without going through `_post_from_ed`. If you find yourself wanting to, you're
|
||||
about to introduce a data race — add a `kind` to the funnel instead.
|
||||
|
||||
- **Background tasks are tracked, both sides.** The gateway's `spawn()` and the
|
||||
engine's `_spawn()` add tasks to a `_tasks` set with a done-callback that
|
||||
discards them, so shutdown can cancel them and the GC can't drop a live
|
||||
coroutine. Launch per-call/background work through these, not a bare
|
||||
`asyncio.create_task` you forget to hold a reference to.
|
||||
|
||||
- **`MockSIPEngine` has no thread.** Tests run against it; it satisfies the same
|
||||
`SIPEngine` interface synchronously/async-inline. When you extend the real
|
||||
engine's behaviour, extend the mock to match, or tests will pass against a
|
||||
fiction.
|
||||
Reference in New Issue
Block a user