Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ named `D-*` row in the ledger at `PLAN.md` section 18.
boundary, even when both API families share the same pending request. V2
permission creation also rejects fields and values outside the frozen
request schema before storing them.
- V2 interrupts now cancel registered provider drains and report whether a
turn was interrupted. Drain capacity and reservation waiters are bounded;
session deletion fences new work and waits for admitted turns before removing
session rows. The delete-side wait is bounded by
`OCSERVE_DRAIN_CANCEL_SECS` (default 30 s) so a drain stuck past the provider
watchdog cannot hold a deletion fence indefinitely. Admission checks run on
the blocking pool, not on a tokio worker.

## 0.7.1 - 2026-11-10

Expand Down
17 changes: 17 additions & 0 deletions MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ tokio::runtime::Builder::new_multi_thread()
borrowed `&str`/`Cow` fields on hot read paths (session list, event encode).
- Strings: `Arc<str>` for shared session/project ids; `internment` only for genuinely
repeated identifiers (tool names, agent names) with a hard intern-table cap.
- V2 drain task registry: at most 128 active session entries, enforced by the
slot semaphore created with `MAX_V2_DRAIN_TASKS`; overflow admission fails
before persisting inbox work (`V2-PLAN.md` P3, `D-V2-DRAIN-CAP`). This is a
COUNT bound, not a memory bound: each active drain can hold up to
`PROMPT_HISTORY_MAX_BYTES`, and the shared prompt-lock map caps concurrent
turns at `MAX_PROMPT_LOCKS` (64), so the worst case is bounded by
min(128, 64) x history, which is why the budget is stated in both places.
- V2 slot reservations can queue at most 128 additional waiters, enforced by
`MAX_V2_DRAIN_WAITERS`; a full waiter cap returns bounded backpressure rather
than retaining another request or background task.
- V2 session deletion fences are capped at 128 concurrent sessions by
`MAX_V2_SESSION_DELETES`; a full cap returns bounded backpressure.
- V2 drains waiting on the shared prompt-lock map are bounded by the same 128
drain slots. The map itself is capped at `MAX_PROMPT_LOCKS` (64); its
`Notify` keeps no per-waiter message queue. Waiters enable their notification
before checking capacity, so each release wakes one registered waiter instead
of coalescing releases into a single stored permit.
- Collections: `SmallVec` for ≤4-element hot vectors; bounded `LruCache` (never `HashMap`
that only grows); caches hold `Weak` where ownership allows (no `Arc` cycles).
- `clippy::unwrap_used` denied in `ocserve-http`/`ocserve-core` (panic=abort ⇒ a panic is an
Expand Down
2 changes: 2 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ current anywhere.
| `D-V2-PERM-EVAL` | `permission.evaluate` is not bridged as a v2 hook; the server's own gate is authoritative. | accepted |
| `D-V2-RPC-EVENTS` | `rpc.events.emit` is not bridged: there is no v2 RPC event SSE surface. | accepted |
| `D-V2-SESSION-LIFECYCLE` | Transport-level v2 hooks (`model.request`, `http.*`, `retry`, `title`, `generate`) are not bridged. | accepted |
| `D-V2-DRAIN-CAP` | At most 128 v2 sessions may hold an active drain slot. A request that needs a new slot receives `ServiceUnavailableError` before its inbox row is written; background wake sources wait for a slot. This bounds the per-session drain task registry and preserves durable admitted work. | accepted |
| `D-V2-STORAGE-MEM` | `ctx.storage` is in-memory for the life of the sidecar; it is not disk-backed. | accepted |
| `D-V2-SESSION-GENERATE` | The transient generate path runs, but the v2 `session.generate` hook is not dispatched. | accepted |
| `D-V2-TOOL-EDITOR` | `tool.transform` registers and records, but the tool registry is static, so it is not applied; `tool.list` returns `[]`. | closed (the callback runs against a live editor; added tools are advertised with their JSON Schema and dispatched; a disposed registration removes its names) |
Expand All @@ -617,6 +618,7 @@ current anywhere.
| `D-TOOLS-TASK` | The `task` subagent tool was advertised by the frozen provider but not implemented. | closed (the tool runs on the shared executor with a depth guard) |
| `D-PERM-SOURCE-KEY` | The permission `source` object uses different keys per contract line: the v1 (1.19.5) SDK type `PermissionV2Source` is `{type,messageID,callID}` while v2 (2.2.5) `Permission.Source` is `{type,messageID,id}`. ocserve emits `id` on the v2 wire and `callID` on the v1 wire; the gate stores the call id under `callID` and each projector renames it. A body cannot satisfy both schemas because both are `additionalProperties:false`. | open |
| `D-PERM-EVENT-NAME` | Upstream 1.19.5 names the v2 permission event `permission.v2.asked`; 2.2.5 names it `permission.asked`. ocserve keeps the v1 name `permission.asked` on every stream and projects the v2 *payload* on `/api/event`, so a 1.19.5-era consumer that dispatches on `permission.v2.asked` will not see it. No shipped 1.19.5 client dispatches on that event: the TUI refreshes permissions over REST, and the web app's adapter maps `permission.v2.asked` only when the server sends it. The 1.19.5/2.2.5 web app also probes `/global/health` first and treats any `{healthy:true}` answer as a v1 server (`server-protocol.ts`), so against ocserve it subscribes to `/global/event` and consumes the v1 payload — the v2 permission frame is reachable only from a client that calls `/api/event` directly. | open |
| `D-V2-INTERRUPT-STATUS` | `/api/session/{id}/interrupt` is in BOTH frozen contracts: v1 declares 204, v2 declares a 200 `SessionInterruptResponse`. One handler cannot return both, and the route is not in the v2-only table, so a v2-disabled server now answers 200 where its own v1 contract says 204. No shipped v1 client calls it (the 1.19.5 v1 SDK uses `/session/{id}/abort`), so the impact is contract-visible only. | open |

Historical rows kept for the tokens' own history, all closed: `D-GENERATE-EPHEMERAL`,
`D-REVERT-NOSNAP`, `D-VCS-NOAPPLY`, `D-HOOK-*` items are in §16.9 and
Expand Down
Loading
Loading