Documentation
¶
Overview ¶
Package protocol implements the `sessions.*` Protocol methods the Console Sessions page consumes:
- sessions.list — paginated, filtered SessionRegistry projection.
- sessions.inspect — full per-session snapshot for the detail view.
- sessions.delete — own-session-only data-lifecycle erasure.
- sessions.set_title — sets/clears a session's human-readable title.
The seam (CLAUDE.md §4.4) ¶
The Service depends on the `Projector` interface, not on a concrete session registry. The V1 production implementation is `ListerProjector` (lister_projector.go) — a thin read-only projection over a `sessions.SessionLister`. A future remote / cross-runtime projector slots in behind the same interface without reshaping the Service.
Identity is mandatory (CLAUDE.md §6 rule 9) ¶
Every method takes the wire request's `IdentityScope`. An incomplete triple fails closed with `ErrIdentityRequired` — there is no identity-downgrading knob. The Service NEVER reads identity from a package-level global; the triple flows in via the request.
Cross-tenant gating ¶
A `sessions.list` whose `Filter.TenantIDs` names a tenant other than the caller's verified tenant requires the verified `auth.ScopeAdmin` claim. The Service receives an `adminScoped bool` the wire handler computes from the verified JWT scope set; a false value on a cross-tenant filter fails closed with `ErrCrossTenantScope`. There is NO `sessions.admin` scope — the closed two-scope set (`admin` + `console:fleet`) is the only admit surface. On a successful admin-scope query the Service emits an `audit.admin_scope_used` event.
Concurrent reuse ¶
A constructed *Service is immutable after NewService and safe to share across N concurrent goroutines: it holds only the Projector reference + an optional bus + redactor + logger; every method's per-call state lives in the call's arguments and locals, never on the Service.
Index ¶
- Variables
- type CounterEnricher
- type CounterEnricherDeps
- type Enricher
- type Eraser
- type ListerProjector
- func (p *ListerProjector) CountersAvailable() bool
- func (p *ListerProjector) InspectSession(ctx context.Context, id identity.Identity, sessionID string, adminScoped bool) (prototypes.SessionsInspectResponse, error)
- func (p *ListerProjector) ListSessions(ctx context.Context, id identity.Identity, f prototypes.SessionFilter, ...) ([]prototypes.SessionRow, error)
- type ListerProjectorOption
- type Option
- type Projector
- type Service
- func (s *Service) Delete(ctx context.Context, req prototypes.SessionsDeleteRequest) (prototypes.SessionsDeleteResponse, error)
- func (s *Service) HasEraser() bool
- func (s *Service) HasTitleSetter() bool
- func (s *Service) Inspect(ctx context.Context, req prototypes.SessionsInspectRequest, adminScoped bool) (prototypes.SessionsInspectResponse, error)
- func (s *Service) List(ctx context.Context, req prototypes.SessionsListRequest, adminScoped bool) (prototypes.SessionsListResponse, error)
- func (s *Service) SetTitle(ctx context.Context, req prototypes.SessionsSetTitleRequest) (prototypes.SessionsSetTitleResponse, error)
- type SessionCounters
- type SessionsAdminQueryPayload
- type TitleSetter
Constants ¶
This section is empty.
Variables ¶
var ( // ErrIdentityRequired — the request carried an incomplete identity // triple. RFC §5.5 / CLAUDE.md §6 rule 9 — fails closed. ErrIdentityRequired = errors.New("sessions/protocol: identity scope incomplete") // ErrCrossTenantScope — a `sessions.list` filter named a tenant // outside the caller's verified tenant without the verified // `auth.ScopeAdmin` claim. ErrCrossTenantScope = errors.New("sessions/protocol: cross-tenant filter requires the admin scope claim") // ErrInvalidRequest — the request was structurally invalid (an // out-of-range limit, an unknown enum, a malformed cursor). ErrInvalidRequest = errors.New("sessions/protocol: invalid request") // ErrSessionNotFound — `sessions.inspect` / `sessions.delete` targeted // a session id with no record visible to the caller's identity scope. ErrSessionNotFound = errors.New("sessions/protocol: session not found") // ErrSessionRunning — `sessions.delete` was refused because the target // session has a RUNNING task (mirroring the GC never-reap-running // invariant). The handler maps it to CodeSessionRunning (409). No // store is touched on refusal. ErrSessionRunning = errors.New("sessions/protocol: cannot erase a session with a running task") // ErrErasureUnsupported — `sessions.delete` reached a Service that was // built without an Eraser (the runtime does not advertise the // CapSessionLifecycle capability). The handler maps it to // CodeUnknownMethod (404) so a client that probed the route detects // the unwired surface exactly as it would a missing route. ErrErasureUnsupported = errors.New("sessions/protocol: session erasure is not wired on this runtime") // ErrErasureRecordFailed — the erasure cascade's destructive steps // completed but the durable `session.erased` record-of-fact could not // be completed (a redactor refusal or a bus-publish failure). // Delete fails the whole call loud rather than reporting success with // a missing audit trail; the session's data IS gone, and a re-invoke // converges (it re-attempts only the record, no destructive step // re-runs). The handler maps it to CodeRuntimeError (500). ErrErasureRecordFailed = errors.New("sessions/protocol: erasure record-of-fact could not be durably completed") // ErrMisconfigured — NewService was called with a nil Projector. ErrMisconfigured = errors.New("sessions/protocol: NewService missing a mandatory dependency") // ErrTitleSetUnsupported — `sessions.set_title` reached a Service that // was built without a TitleSetter. The handler maps it to // CodeUnknownMethod (404), the same posture as ErrErasureUnsupported. ErrTitleSetUnsupported = errors.New("sessions/protocol: session title-set is not wired on this runtime") )
Sentinel errors the Service returns. The wire handler maps each onto a canonical Protocol Code + HTTP status; in-process callers compare with errors.Is.
Functions ¶
This section is empty.
Types ¶
type CounterEnricher ¶ added in v1.14.0
type CounterEnricher struct {
// contains filtered or unexported fields
}
CounterEnricher is the V1 production Enricher. It aggregates the per-session counters from raw data owned by subsystems one package over — SUMMING, not reading a shadow store:
- total_cost_cents / total_tokens: summed from `llm.cost.recorded` events scoped to the session (via the event substrate).
- events_count: the count of the session's events from the durable event substrate (HistoryReplayer.ListWindow, bounded).
- tasks_count / has_failed_task: the session's tasks from the task registry.
- has_pending_intervention: a paused pause record from the pause registry scoped to the session.
Concurrent reuse (CLAUDE.md §5) ¶
A constructed *CounterEnricher is immutable after NewCounterEnricher: it holds only the bus / registry / coordinator references (each itself safe for concurrent reuse) plus a logger. Every Counters call's per-run state lives in its arguments and locals; the enricher reads nothing from itself for run-specific data.
func NewCounterEnricher ¶ added in v1.14.0
func NewCounterEnricher(deps CounterEnricherDeps) (*CounterEnricher, error)
NewCounterEnricher builds the V1 production Enricher. Every dependency is mandatory — a nil Bus / Tasks / Pauses fails loud with ErrMisconfigured rather than building an enricher that reports believable-but-false zeros on one dimension (CLAUDE.md §5). The returned *CounterEnricher is immutable and safe for concurrent reuse.
func (*CounterEnricher) Counters ¶ added in v1.14.0
func (e *CounterEnricher) Counters(ctx context.Context, id identity.Identity, sessionID string) SessionCounters
Counters implements Enricher for the production backend. It reads the session's own full triple (id) to scope every source — no cross-session bleed: the authorisation to see this session already happened at the projector's identity-scope predicate; the enricher re-scopes each read to the ROW's own identity so an admin listing another tenant's session reads exactly that session's tasks / events / pauses.
type CounterEnricherDeps ¶ added in v1.14.0
type CounterEnricherDeps struct {
// Bus is the event substrate the cost / tokens / events counters read
// from. It MUST implement events.HistoryReplayer to serve the windowed
// per-session scan; a bus without it yields honest-partial counters
// (Partial=true, zero bus-derived counts), never a silent zero.
Bus events.EventBus
// Tasks is the task registry the tasks_count / has_failed_task counters
// read from, scoped to the session.
Tasks tasks.TaskRegistry
// Pauses is the pause coordinator the has_pending_intervention counter
// reads from, scoped to the session.
Pauses pauseresume.Coordinator
// Logger receives Warn-level diagnostics when a bounded scan truncates
// or the substrate is unavailable (the degradation is NEVER silent —
// it is both logged and surfaced as SessionCounters.Partial). Nil
// routes to slog.Default().
Logger *slog.Logger
}
CounterEnricherDeps carries the CounterEnricher's mandatory dependencies. All three are required — a nil dependency would silently drop a counter dimension (a value that reads zero on a busy session), the exact silent-absence class this phase closes, so NewCounterEnricher fails loud rather than build a half-blind enricher (CLAUDE.md §5).
type Enricher ¶ added in v1.14.0
type Enricher interface {
// Counters returns the read-time counter rollup for one session,
// aggregated from the cost-event stream, the task registry, the event
// substrate, and the pause registry — all identity-scoped to `id` (the
// session's own full triple). A zero-valued return is honest ("we don't
// have this data"), never silent degradation. When the bounded
// per-session scan hits its bound (or a retention gap), the returned
// SessionCounters.Partial is set and the cost / tokens / events counts
// are an HONEST LOWER BOUND, never a plausible exact number.
Counters(ctx context.Context, id identity.Identity, sessionID string) SessionCounters
}
Enricher is the optional read-time counter-rollup backend the ListerProjector overlays onto a SessionRow. It mirrors the shipped tasks.Projector enricher SEAM — only the seam is inherited; the aggregation below is net-new (the tasks serve enricher returns a ZERO cost rollup, deferring cost to the event stream). Production wiring supplies a CounterEnricher backed by the event substrate + task registry + pause registry; tests and partial builds run without one.
A projector with no Enricher wired reports honest ZERO counters — "we don't have this data", not a silent degradation of a known value — and the Service loud-rejects a facet/sort over those counters (WARN-3, see protocol.go) so an unwired build can never reproduce the false-absence defect this phase closes.
type Eraser ¶ added in v1.7.0
type Eraser interface {
// Erase performs the full erasure cascade for the verified identity
// (own-session-only — the scope contract is enforced at the Service
// edge before Erase is reached). Returns the deletion telemetry, or a
// refusal: sessions.ErrSessionRunning (a RUNNING task) /
// sessions.ErrSessionNotFound (absent under the caller's identity).
Erase(ctx context.Context, id identity.Identity) (prototypes.SessionsDeleteResponse, error)
}
Eraser is the seam the Service depends on for `sessions.delete`. The V1 production implementation is the in-runtime cascade orchestrator (`sessions.CascadeEraser`), which performs the real three-store cascade + session-record delete for the verified identity, refusing fail-loud on a running task. The Service depends ONLY on this interface (CLAUDE.md §4.4) — a future remote / cross-runtime eraser slots in behind it without reshaping the Service.
type ListerProjector ¶
type ListerProjector struct {
// contains filtered or unexported fields
}
ListerProjector is the V1 production Projector — a thin read-only projection over a `sessions.SessionLister` (the Registry's `ListSnapshots` surface). It maps the runtime `sessions.SessionSnapshot` onto the flat Protocol `SessionRow` wire shape (RFC §5.1 single-source rule: the Console never reads `sessions.Session`).
Identity scoping (CLAUDE.md §6) ¶
ListSessions builds the `sessions.SessionListFilter` so the registry scopes by tenant: a non-admin caller is restricted to its own `(tenant, user)`; an admin caller MAY widen via the request's `TenantIDs`. The registry's `ListSnapshots` does NOT re-check scope — the gate is the Service's `ErrCrossTenantScope` check; this projector only translates the gate decision into the filter shape.
Enrichment seam (CLAUDE.md §4.4) ¶
The SessionLister owns the lifecycle fields (status, timestamps, title, identity); it does NOT model the per-session cost / token / task / event counters or a session→agent binding. ListerProjector reads the counters through the optional Enricher seam (enricher.go), mirroring the shipped tasks.Projector enricher. When no Enricher is wired, the counters stay ZERO — honest ("we don't have this data"), not silent degradation — and the Service loud-rejects a facet/sort over them (WARN-3) so an unwired build never returns a false-empty counter page.
Concurrent reuse ¶
A constructed *ListerProjector is immutable after NewListerProjector and safe to share across N concurrent goroutines — it holds only the SessionLister + optional Enricher references (both themselves safe for concurrent reuse); every method's per-call state lives in the call's arguments and locals.
func NewListerProjector ¶
func NewListerProjector(lister sessions.SessionLister, opts ...ListerProjectorOption) (*ListerProjector, error)
NewListerProjector builds the V1 Projector over a SessionLister. The lister is mandatory — a nil fails loud rather than building a projector that nil-panics on the first request (CLAUDE.md §5).
func (*ListerProjector) CountersAvailable ¶ added in v1.14.0
func (p *ListerProjector) CountersAvailable() bool
CountersAvailable reports whether the projector populates the numeric / boolean counters (cost / tokens / tasks / events / intervention / failed-task) — i.e. whether an Enricher is wired. False on a partial build: the Service then loud-rejects a facet / sort over those counters rather than returning a false-empty page (WARN-3).
func (*ListerProjector) InspectSession ¶
func (p *ListerProjector) InspectSession(ctx context.Context, id identity.Identity, sessionID string, adminScoped bool) (prototypes.SessionsInspectResponse, error)
InspectSession implements Projector.InspectSession. It lists the one session id and projects the snapshot plus the (currently empty) recent-interventions / recent-artifacts slices.
V1 scope note: the recent-interventions / recent-artifacts cards are fed by the Console's own event-stream subscription on the detail route (the page consumes `pause.*` / `artifacts.*` events filtered to the session — page spec §5). `sessions.inspect` ships the Row projection + empty capped slices; the cards populate from the live event stream client-side. A future StateStore-backed enrichment can pre-fill the slices without a wire-shape break (the fields are already on the response).
func (*ListerProjector) ListSessions ¶
func (p *ListerProjector) ListSessions(ctx context.Context, id identity.Identity, f prototypes.SessionFilter, adminScoped bool) ([]prototypes.SessionRow, error)
ListSessions implements Projector.ListSessions. It builds the identity-scoped registry filter, lists the snapshots, and projects each onto a SessionRow.
type ListerProjectorOption ¶ added in v1.14.0
type ListerProjectorOption func(*ListerProjector)
ListerProjectorOption configures NewListerProjector.
func WithEnricher ¶ added in v1.14.0
func WithEnricher(e Enricher) ListerProjectorOption
WithEnricher wires the read-time counter-rollup backend. A nil enricher is treated as "WithEnricher not supplied" — the projector ships honest ZERO counters and reports CountersAvailable()==false, so the Service loud-rejects a facet/sort over the counters rather than returning a false-empty page (WARN-3).
type Option ¶
type Option func(*Service)
Option configures NewService.
func WithBus ¶
WithBus wires the canonical events.EventBus the Service publishes the `audit.admin_scope_used` event onto when an admin-scope query succeeds. A nil bus is treated as "WithBus not supplied" — the admin path still works, but the audit observation is logged at Info instead of published (the admin action is NEVER fully silent — CLAUDE.md §13).
func WithEraser ¶ added in v1.7.0
WithEraser wires the Eraser the Service dispatches `sessions.delete` to. When unsupplied (or nil) the Service answers `sessions.delete` with ErrErasureUnsupported (the handler maps it to a 404) and the runtime does NOT advertise the CapSessionLifecycle capability — capability gating: a runtime that did not wire an eraser is honestly read-only on the Sessions surface. A non-nil eraser enables the erasure path.
func WithLogger ¶
WithLogger sets the slog.Logger the Service logs admin actions and audit-emit failures to. A nil logger routes to slog.Default().
func WithRedactor ¶
WithRedactor wires the audit.Redactor the Service runs the `audit.admin_scope_used` payload through before publishing. A nil redactor is treated as "WithRedactor not supplied".
func WithTitleSetter ¶ added in v1.12.0
func WithTitleSetter(ts TitleSetter) Option
WithTitleSetter wires the TitleSetter the Service dispatches `sessions.set_title` to. When unsupplied (or nil) the Service answers `sessions.set_title` with ErrTitleSetUnsupported (the handler maps it to a 404) — a runtime that did not wire a title setter is honestly read-only on session titles. A non-nil setter enables the write path.
type Projector ¶
type Projector interface {
// ListSessions returns every session row visible to the caller,
// already identity-scoped: when adminScoped is false the
// implementation MUST restrict to the caller's own (tenant, user);
// when true it MAY honour a cross-tenant TenantIDs filter. The
// Service applies the facet filter + sort + pagination on top.
ListSessions(ctx context.Context, id identity.Identity, f prototypes.SessionFilter, adminScoped bool) ([]prototypes.SessionRow, error)
// InspectSession returns the full snapshot for sessionID, or
// ErrSessionNotFound. adminScoped widens the lookup across tenants.
InspectSession(ctx context.Context, id identity.Identity, sessionID string, adminScoped bool) (prototypes.SessionsInspectResponse, error)
// CountersAvailable reports whether the projector populates the
// numeric / boolean counters (cost / tokens / tasks / events /
// intervention / failed-task). False when no counter Enricher is wired:
// the Service then loud-rejects a facet / sort over those counters
// rather than returning a false-empty page (WARN-3).
CountersAvailable() bool
}
Projector is the read seam the Service depends on. The V1 production implementation is ListerProjector. Every method takes the verified identity triple plus the resolved admin-scope flag so the implementation scopes its reads.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements the `sessions.*` Protocol methods. It is a safe for concurrent reuse compiled artifact — immutable after NewService.
func NewService ¶
NewService builds the Sessions Protocol service over a Projector. The projector is mandatory — a nil fails loud with ErrMisconfigured rather than building a Service that would nil-panic on the first request (CLAUDE.md §5). The returned *Service is immutable after construction and safe for concurrent use by N goroutines.
func (*Service) Delete ¶ added in v1.7.0
func (s *Service) Delete(ctx context.Context, req prototypes.SessionsDeleteRequest) (prototypes.SessionsDeleteResponse, error)
Delete implements the `sessions.delete` method — the identity-scoped, own-session-only data-lifecycle erasure of a whole session and its scoped State, Memory, and Artifacts.
The scope contract is own-session-only: the request identity IS the caller's verified identity (the wire handler overlays the verified triple and rejects any body-identity mismatch as identity_required before this method is reached), so there is no admin / cross-tenant path. A nil eraser (the capability was not wired) is reported as ErrErasureUnsupported. A refusal on a RUNNING task surfaces as ErrSessionRunning (409); an absent session as ErrSessionNotFound (404).
func (*Service) HasEraser ¶ added in v1.7.0
HasEraser reports whether the Service was wired with an Eraser — i.e. whether `sessions.delete` is supported and the runtime should advertise the CapSessionLifecycle capability. The wiring layer uses it to gate the capability advertisement so a read-only runtime stays honest.
func (*Service) HasTitleSetter ¶ added in v1.12.0
HasTitleSetter reports whether the Service was wired with a TitleSetter — i.e. whether `sessions.set_title` is supported. The wiring layer can use it exactly like HasEraser to gate any future capability advertisement.
func (*Service) Inspect ¶
func (s *Service) Inspect(ctx context.Context, req prototypes.SessionsInspectRequest, adminScoped bool) (prototypes.SessionsInspectResponse, error)
Inspect implements the `sessions.inspect` method — the full per-session snapshot the Console Sessions detail view renders.
func (*Service) List ¶
func (s *Service) List(ctx context.Context, req prototypes.SessionsListRequest, adminScoped bool) (prototypes.SessionsListResponse, error)
List implements the `sessions.list` method. It validates identity, enforces the cross-tenant gate, resolves the identity-scoped rows from the Projector, applies the facet filter + sort + cursor pagination, and emits an `audit.admin_scope_used` event on a successful admin-scope query.
func (*Service) SetTitle ¶ added in v1.12.0
func (s *Service) SetTitle(ctx context.Context, req prototypes.SessionsSetTitleRequest) (prototypes.SessionsSetTitleResponse, error)
SetTitle implements the `sessions.set_title` method — sets or clears a session's human-readable title.
The write scope is the owning `(tenant, user)`: req.Identity's tenant/user MUST equal the caller's verified identity (the wire handler overlays the verified triple and rejects any body-identity mismatch as identity_required before this method is reached, mirroring `sessions.delete`). req.SessionID is a DEDICATED field and MAY name a sibling session of the caller's own `(tenant, user)` — the SessionID component of req.Identity itself is unused for targeting (only its tenant/user matter). A nil TitleSetter (the capability was not wired) is reported as ErrTitleSetUnsupported.
type SessionCounters ¶ added in v1.14.0
type SessionCounters struct {
// TasksCount is the number of tasks the session has spawned.
TasksCount int
// EventsCount is the number of events the session has emitted (a lower
// bound when Partial is set).
EventsCount int
// TotalCostCents is the session's accumulated LLM cost in US cents (a
// lower bound when Partial is set).
TotalCostCents int64
// TotalTokens is the session's accumulated LLM token count (a lower
// bound when Partial is set).
TotalTokens int64
// HasPendingIntervention reports whether the session has a pause
// awaiting resume / approval.
HasPendingIntervention bool
// HasFailedTask reports whether the session has at least one failed
// task.
HasFailedTask bool
// Partial is true when the bounded per-session scan that produced the
// cost / tokens / events counts hit its bound (ListWindow HasMore /
// Truncated) or the windowed-read substrate was unavailable. The counts
// are then an HONEST LOWER BOUND, not exact — the facet/sort layer MUST
// NOT treat a Partial key as authoritative. TasksCount / HasFailedTask /
// HasPendingIntervention (from the registries, not the bounded scan)
// stay exact.
Partial bool
}
SessionCounters is the read-time counter rollup for one session — the six false-absence SessionRow counters plus the honest-partial marker.
type SessionsAdminQueryPayload ¶
type SessionsAdminQueryPayload struct {
events.SafeSealed
// Actor is the verified admin identity at the Protocol edge — the
// (tenant, user, session) triple the JWT carried.
Actor identity.Identity
// Method is the Protocol method that carried the cross-tenant query
// (`sessions.list` or `sessions.inspect`).
Method string
}
SessionsAdminQueryPayload is the typed SafePayload published on the canonical `audit.admin_scope_used` event when an operator runs a cross-tenant `sessions.list` / `sessions.inspect` query under the verified `auth.ScopeAdmin` claim.
SafePayload by construction: every field is a bounded identity component or a Protocol method name — no caller-supplied bytes reach the bus. The Sessions wire surface rejects malformed requests at the Protocol edge before the emit.
The payload is distinct from `auth.AdminScopeUsedPayload` ( impersonation), `events.AdminScopeUsedPayload` (Subscribe), and `toolsprotocol.ToolsAdminActionPayload` — all ride the same canonical `audit.admin_scope_used` event type, but each emit source declares its own typed payload. A subscriber type-switches.
type TitleSetter ¶ added in v1.12.0
type TitleSetter interface {
// SetTitle sets or clears the title of session `id` for the CALLER's
// verified `(tenant, user)` (`ident`). See sessions.SessionRegistry's
// SetTitle for the full semantics (trim, validate, empty clears,
// (tenant, user) write scope). Returns sessions.ErrInvalidTitle,
// sessions.ErrSessionNotFound, or sessions.ErrIdentityMismatch on
// refusal.
SetTitle(ctx context.Context, id string, ident identity.Identity, title string) error
}
TitleSetter is the write seam the Service dispatches `sessions.set_title` to. The V1 production implementation is `*sessions.Registry` — its SetTitle method satisfies this interface directly (Registry already implements the wider SessionRegistry, of which SetTitle is one method). The Service depends ONLY on this narrow interface (CLAUDE.md §4.4) — a future remote / cross-runtime title-set slots in behind it without reshaping the Service.