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
6 changes: 4 additions & 2 deletions llp/0049-hypignore-usage-policy.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ dotfile should not be the only path:
([LLP 0031](./0031-layered-config.decision.md)) or pushed by central
([LLP 0036](./0036-central-config-driven-client-actions.decision.md)).
Org-forced policy is a future concern tied to `local-only`.
4. **Ephemeral per-session opt-out** is not in V1 — see
[LLP 0051](./0051-usage-policy-future-extensions.decision.md).
4. **Ephemeral per-session opt-out** is a separate mechanism, not part of this
folder spec. It is specced in [LLP 0062](./0062-session-opt-out.spec.md)
(session-scoped, in-memory, keyed on `session_id`), promoted from the deferred
sketch in [LLP 0051](./0051-usage-policy-future-extensions.decision.md#session-opt-out).

## Requirements {#requirements}

Expand Down
8 changes: 7 additions & 1 deletion llp/0050-ignore-enforced-in-adapters.decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Systems:** Gateway, Plugins, Core
**Author:** Phil / Claude
**Date:** 2026-06-29
**Related:** LLP 0012, LLP 0016, LLP 0049
**Related:** LLP 0012, LLP 0016, LLP 0049, LLP 0062

> The `.hypignore` capture-seam drop ([LLP 0049](./0049-hypignore-usage-policy.spec.md))
> lives in the `@hypaware/claude` and `@hypaware/codex` adapters — the only
Expand Down Expand Up @@ -91,3 +91,9 @@ worse coupling than both importing core.
- The gateway source and recorder are not modified.
- A future caller-supplied `cwd` for raw-proxy traffic would add a *new* call
site that reuses the same core matcher — no change to this decision.
- The ephemeral per-session opt-out ([LLP 0062](./0062-session-opt-out.spec.md))
reuses this same adapter drop with a *different key*: it matches on the
`session_id` the adapter resolves instead of on `cwd`, and returns the same
`USAGE_POLICY_DROP` sentinel. That mechanism adds a gateway *control route* and
an in-memory set of opaque `session_id` strings, but the gateway still performs
no drop and interprets no identity, so this decision holds unchanged.
45 changes: 27 additions & 18 deletions llp/0051-usage-policy-future-extensions.decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
**Systems:** Gateway, Sinks, Plugins
**Author:** Phil / Claude
**Date:** 2026-06-29
**Related:** LLP 0014, LLP 0029, LLP 0030, LLP 0039, LLP 0049, LLP 0050
**Related:** LLP 0014, LLP 0029, LLP 0030, LLP 0039, LLP 0049, LLP 0050, LLP 0062

> Two capabilities deliberately **out of V1 scope** for the hypignore mechanism
> ([LLP 0049](./0049-hypignore-usage-policy.spec.md)), captured so the V1 design
> stays forward-compatible with them: the `local-only` usage class, and the
> ephemeral per-session opt-out. This document records *that they are deferred*
> and *the seam each will use*, not a commitment to build them.
>
> **Update (2026-07-03):** the ephemeral per-session opt-out has since been
> promoted to its own spec, [LLP 0062](./0062-session-opt-out.spec.md); this
> document retains it only as historical context ([§session-opt-out](#session-opt-out)).
> `local-only` remains deferred.

## Why this is written now

Expand Down Expand Up @@ -49,33 +54,37 @@ encounters it resolves to `ignore` via the
[fail-safe](./0049-hypignore-usage-policy.spec.md#fail-safe), so no data marked
`local-only` is ever exported by a version that cannot honor the restriction.

## Deferred 2: ephemeral per-session opt-out {#session-opt-out}
## Promoted: ephemeral per-session opt-out {#session-opt-out}

**Intent.** "Don't record *this conversation*" — a temporary, in-memory,
**Promoted to [LLP 0062](./0062-session-opt-out.spec.md) on 2026-07-03.** The
authoritative spec for this mechanism now lives there; the notes below are the
original deferred sketch, kept for provenance.

**Intent.** "Don't record *this conversation*": a temporary, in-memory,
session-scoped drop that does not write a committable file and reverses when the
session ends. This is what the `hypaware-ignore` / `hypaware-unignore` skills
*originally* advertised (`POST` / `DELETE /_hypaware/ignore/session`), against an
endpoint that was never built.
advertise (`POST` / `DELETE /_hypaware/ignore/session`), against an endpoint that
was never built.

**Why it is distinct from `.hypignore`.** It is a different product: *session*-
scoped and *ephemeral*, versus the folder `.hypignore` which is *directory*-scoped
and *persistent/committable* (it stops recording the whole tree for everyone).
Repointing the skills at `.hypignore` would over-broaden "ignore this session"
into "ignore this repo forever," so the session opt-out stays a separate future
into "ignore this repo forever," so the session opt-out stays a separate
mechanism.

**Seam.** Unlike folder rules, a session opt-out keys off the **`session_id`**,
which the gateway *does* see inline on the wire — so this one **can** live in the
gateway (a small in-memory set + the control route), and does **not** contradict
[LLP 0050](./0050-ignore-enforced-in-adapters.decision.md)'s "gateway is
cwd-blind": session-id is not cwd.

**V1 interim.** The two skills are reconciled to be honest — they point at
`hyp ignore` / `.hypignore` and state plainly that per-session in-memory opt-out
is not yet available — rather than POSTing to a 404.
**Seam (refined in [LLP 0062](./0062-session-opt-out.spec.md#enforcement)).** The
original sketch put the whole thing in the gateway. LLP 0062 splits it: the
**control route + in-memory set** are gateway-resident (the gateway holds opaque
`session_id` strings it never interprets), but the **drop itself stays in the
client adapter projector**, keyed on the `session_id` the adapter already
resolves and returning the same `USAGE_POLICY_DROP` sentinel as the `.hypignore`
drop. That keeps [LLP 0050](./0050-ignore-enforced-in-adapters.decision.md)
intact rather than moving provider awareness into the gateway.

## Status

Both are **Draft / not scheduled.** When either is built, this document either
spawns a dedicated spec/decision and is superseded, or is promoted in place. It
exists today so V1's reviewers can confirm the V1 design does not foreclose them.
`local-only` ([§local-only](#local-only)) is **Draft / not scheduled**. The
ephemeral per-session opt-out ([§session-opt-out](#session-opt-out)) has been
**promoted to [LLP 0062](./0062-session-opt-out.spec.md)**. This document exists
today so V1's reviewers can confirm the V1 design does not foreclose either.
179 changes: 179 additions & 0 deletions llp/0062-session-opt-out.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# LLP 0062: ephemeral per-session opt-out

**Type:** Spec
**Status:** Accepted
**Systems:** Gateway, Plugins, Sources
**Author:** Brendan / Claude
**Date:** 2026-07-03
**Related:** LLP 0016, LLP 0030, LLP 0049, LLP 0050, LLP 0051

> "Don't record *this conversation*": a temporary, in-memory, session-scoped
> drop that writes no committable file and reverses when the session ends or on
> `/hypaware-unignore`. The `@hypaware/claude` `hypaware-ignore` /
> `hypaware-unignore` skills already specify the contract (`POST` / `DELETE`
> `/_hypaware/ignore/session`, keyed on `session_id`); this spec makes the code
> honor it. Promotes [LLP 0051 §session-opt-out](./0051-usage-policy-future-extensions.decision.md#session-opt-out)
> from deferred to specced. Distinct from the folder-scoped `.hypignore`
> ([LLP 0049](./0049-hypignore-usage-policy.spec.md)).

## Motivation

The `hypaware-ignore` / `hypaware-unignore` skills advertise a clear, correct
contract: stop recording the current conversation by `POST`ing its session id to
`/_hypaware/ignore/session`, and reverse with `DELETE`. **The contract is right;
the code was never built.** The endpoint, the in-memory drop set, and the
gateway control path they depend on do not exist, so a user invoking the skill
today hits a route nothing serves ([issue #220](https://github.com/hyparam/hypaware/issues/220)).

This spec closes that gap without changing the skills. The skills are the
contract; the implementation is specified here.

## Relationship to `.hypignore` {#vs-hypignore}

This is a **different product** from the folder mechanism, not a variant of it:

| | `.hypignore` ([LLP 0049](./0049-hypignore-usage-policy.spec.md)) | session opt-out (this spec) |
|---|---|---|
| Scope | a directory subtree | one client session |
| Lifetime | persistent, committable | ephemeral, in-memory |
| Audience | the whole tree, for everyone | just the current conversation |
| Match key | `cwd` (ancestor walk) | `session_id` |

Repointing the skills at `.hypignore` would over-broaden "ignore this session"
into "ignore this repo forever," which is why the two stay separate mechanisms.
They are also **independent at enforcement time**: either match suppresses; they
do not merge or interact.

## The match key is `session_id` {#scope}

The drop keys on **`session_id`**, the always-present partition key
([LLP 0030](./0030-session-id-partition-key.decision.md)). What that scope means
differs per client, and the difference is load-bearing:

| Client | `session_id` | `conversation_id` | A `session_id` drop suppresses |
|---|---|---|---|
| Claude | the whole session (`x-claude-code-session-id` / `metadata.user_id.session_id`) | `null` (the session *is* the thread) | exactly this conversation |
| Codex | the session container (`metadata.session_id` / `session-id` header) | the thread within it | **all** threads in that session |

For **Claude**, `session_id == the conversation`, so the drop is exact. For
**Codex**, `session_id` is a container of multiple `conversation_id` threads, so
a `session_id` drop is broader than "this conversation": it suppresses every
thread in the session. Per-thread (`conversation_id`) granularity is a
[non-goal](#non-goals); the over-drop is latent, not live, because the only
opt-out skill today is Claude-only and Claude has no `conversation_id`.

## Enforcement: control route in the gateway, drop in the adapter {#enforcement}

The naive reading of "gateway-resident" (as [LLP 0051](./0051-usage-policy-future-extensions.decision.md#session-opt-out)
originally phrased it) would have the gateway itself perform the drop. That would
force the gateway to obtain `session_id` from the request, either by parsing the
provider-specific body (`metadata...session_id`) or by trusting a
provider-specific header. Both push provider awareness into the gateway, which
[LLP 0050](./0050-ignore-enforced-in-adapters.decision.md) forbids, and the
header path risks diverging from the body-first canonical id the row is actually
stamped with.

**So the work splits across the same seam `.hypignore` already uses:**

1. **Control surface: gateway.** The gateway serves `POST` / `DELETE
/_hypaware/ignore/session` and holds an in-memory set of **opaque session-id
strings**. It never interprets them: to the gateway they are meaningless
tokens toggled on and off. This is provider-agnostic and does not violate
[LLP 0050](./0050-ignore-enforced-in-adapters.decision.md) (`session_id` is
not `cwd`, and the gateway inspects nothing about the exchange).

2. **Drop: client adapter exchange projector.** The adapter already resolves the
canonical `session_id` it stamps on the row (`resolveClaudeSessionId` for
Claude, `metadata.session_id` for Codex). When that `session_id` is in the
ignored set, the projector returns the terminal `USAGE_POLICY_DROP` sentinel,
exactly as the `.hypignore` `cwd` drop does
([LLP 0050](./0050-ignore-enforced-in-adapters.decision.md)). The gateway
dispatcher already recognizes that sentinel, persists nothing, and logs an
intentional usage-policy drop rather than a `no_projector_match` miss.

This is the key reconciliation: **the session opt-out does not overturn
[LLP 0050](./0050-ignore-enforced-in-adapters.decision.md); it adds a second
match key (`session_id`) feeding the same adapter drop.** `.hypignore` matches on
`cwd`; session opt-out matches on `session_id`; both terminate in
`USAGE_POLICY_DROP` returned from the adapter. Only the *control surface* is new
and gateway-resident.

Matching on the adapter's own resolved `session_id` (not a gateway header peek)
also guarantees the dropped identity is the recorded identity: the skill sends
`CLAUDE_CODE_SESSION_ID`, which is the same value the Claude adapter resolves and
stamps, so the set membership test cannot drift from what would have been
written.

### Gateway control-path concept {#control-path}

Today `ai-gateway`'s source compiles only an *upstream* routing table (which API
to proxy to) and treats every inbound request as proxiable
(`ai-gateway/src/source.js`, `proxy.js`). Serving the endpoint requires a new
concept: requests under the reserved **`/_hypaware/`** prefix are recognized as
**local control requests**, handled in-process, and never forwarded upstream.
The prefix is reserved for this and future control endpoints.

## Ephemerality {#ephemeral}

The ignored-session set lives only in the running gateway's memory. A gateway
restart drops the set, and recording silently resumes for the affected session:
the skill notes already state this and advise re-running `/hypaware-ignore` after
a restart. This is accepted, not a defect: the opt-out is deliberately a
lightweight session convenience, and the committable, durable mechanism is
`.hypignore` ([LLP 0049](./0049-hypignore-usage-policy.spec.md)).

## Non-goals {#non-goals}

1. **Per-thread (`conversation_id`) granularity.** Deferred. `conversation_id`
is `null` for Claude and, for Codex, is computed during projection from a
provider-specific body, so keying on it would pull provider parsing into the
gateway and contradict [LLP 0050](./0050-ignore-enforced-in-adapters.decision.md).
If a Codex opt-out ever needs true per-thread grain, it follows the
adapter-enforcement model keyed on `conversation_id` and is specced
separately; it does not motivate moving the drop into the gateway. Until
then, a Codex `session_id` drop over-drops to the whole session
(see [scope](#scope)).
2. **No persistence or committable form.** That is `.hypignore`
([LLP 0049](./0049-hypignore-usage-policy.spec.md)). This mechanism writes no
file and does not survive restart.
3. **Prospective-only; no purge.** Only exchanges arriving while the session is
ignored are dropped. Rows already recorded before the opt-out are left
untouched; retroactive deletion is out of scope, matching
[LLP 0049](./0049-hypignore-usage-policy.spec.md#prospective-only).
4. **No central/config interaction.** The opt-out is a local, in-memory toggle.
It is not layered config ([LLP 0031](./0031-layered-config.decision.md)) and
is not pushed by central.

## Requirements {#requirements}

- **R1.** `POST /_hypaware/ignore/session` with `{"session_id": "..."}` MUST add
that id to the gateway's in-memory ignored-session set; `DELETE` with the same
body MUST remove it. Both MUST be idempotent and MUST return the current total
count (the skill reads `.total`).
- **R2.** The gateway MUST recognize `/_hypaware/*` as local control paths and
MUST NOT proxy them upstream (see [control path](#control-path)).
- **R3.** The ignored-session set MUST be in-memory only: no file, no cache
column, lost on gateway restart (see [ephemerality](#ephemeral)).
- **R4.** Enforcement MUST be a capture-seam drop in the client adapter exchange
projector, returning the same `USAGE_POLICY_DROP` sentinel as the `.hypignore`
drop ([LLP 0050](./0050-ignore-enforced-in-adapters.decision.md)), so nothing
is written and the gateway logs an intentional drop, not a projector miss.
- **R5.** The match key MUST be the `session_id` the adapter resolves and stamps
on the row (body-first canonical resolution), NOT a gateway-side header peek,
so the dropped set matches the recorded identity.
- **R6.** The opt-out MUST NOT alter the live LLM call: the response has already
been streamed by projection time, so only persistence is suppressed (matching
[LLP 0049 R2](./0049-hypignore-usage-policy.spec.md#requirements)).
- **R7.** session opt-out and folder `.hypignore` MUST be independent: either
match suppresses; they do not merge.
- **R8.** Tests MUST cover Claude (session equals conversation), Codex (whole
session versus a single thread, documenting the over-drop), and
restart-drops-state.

## `@ref` annotations code will carry {#refs}

- The gateway control route and the ignored-session set:
`@ref LLP 0062#control-path [implements]` and `@ref LLP 0062#ephemeral`.
- The adapter projector drop keyed on `session_id`:
`@ref LLP 0062#enforcement [implements]`, alongside the existing
`@ref LLP 0050` on the same drop site.