protocol

package
v1.28.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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.

Projection selector

Both read methods carry an additive `Projection` selector (omitted ⇒ the original "full" projection). `projection=lifecycle` asks for the session catalog fields with NO counter / history / task / pause enrichment: the request is served from the catalog projection / page path before any Enricher call, the rows carry `CounterStatus=not_requested`, and a counter-dependent filter or sort is rejected `invalid_request` regardless of enricher wiring (there are no counters to narrow or order by). Identity scoping, session reach, admin widening + audit, not-found posture, and ordering / cursor semantics are identical to the full projection.

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

Constants

This section is empty.

Variables

View Source
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. Every row's explicit counter availability is marked via SessionRow.CounterStatus (current / partial / not_requested / unavailable) so a zero counter never reads as a measured zero.

Lifecycle-only projection

`projection=lifecycle` requests (pageLifecycleOnly / inspectLifecycleOnly) are served from the catalog projection / page path BEFORE any Enricher call: the counters are never read, stay zero, and are marked CounterStatus=not_requested. The filter / sort / cursor / truncation and the identity-scope / admin-widening semantics are exactly the full projection's — only the counter payload is absent.

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

func WithBus(b events.EventBus) Option

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

func WithEraser(e Eraser) Option

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

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog.Logger the Service logs admin actions and audit-emit failures to. A nil logger routes to slog.Default().

func WithRedactor

func WithRedactor(r audit.Redactor) Option

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 ProjectionEnricher added in v1.28.0

type ProjectionEnricher struct {
	// contains filtered or unexported fields
}

ProjectionEnricher is the projection-backed Enricher adapter: it serves the per-session counter rollup from the authoritative durable observability rollup projection (internal/observability/rollups) when that projection is CURRENT and its retained horizon COVERS the session's lifetime, and otherwise delegates explicitly to the existing raw CounterEnricher bounded scan — the honest fallback. The projection is used for EXACTLY the dimensions the projection is authoritative for (COST, TOKENS, and FAILED TASK OUTCOMES); the dimensions the projection does not model (total events emitted, total tasks spawned, and pending intervention) are read from the raw bounded scan, and the two are merged deterministically.

What the projection backs — and what it does NOT

The rollup projection is an indexed materialization of the canonical event log's supported measures. Its measures are SOURCE-BACKED deltas, and the adapter never maps a measure onto a public counter whose meaning is broader than the measure's:

  • TotalCostCents ← llm_cost_micros (exact integer micro-units of USD, converted to the existing whole-cent wire representation with ONE deterministic rounding at the end — sub-cent calls never floor to 0).
  • TotalTokens ← llm_tokens_total.
  • HasFailedTask ← tasks_failed > 0 (failed TERMINAL OUTCOMES).

The projection does NOT back events_count or tasks_count:

  • llm_completions is the session's `llm.cost.recorded` successful-completion count — a SUBSET of the events the session emitted (task / pause / artifact / lifecycle events are not completions), so presenting it as total events_count would be a believable-but-false undercount.
  • tasks_completed + tasks_failed + tasks_cancelled counts terminal OUTCOMES, not the tasks the session SPAWNED (running / paused tasks never produce a terminal outcome), so presenting it as total tasks_count would be a believable-but-false undercount too.

The canonical totals for those two counters — total events emitted and total spawned tasks — plus has_pending_intervention (the projection does not model the pause registry) are read through the existing raw bounded CounterEnricher fallback seam on EVERY projection-backed path. The raw scan's Partial flag rides along: a truncated event scan, an unreadable registry read, or an unreadable pause read makes the aggregate CounterStatus=partial, never current.

Deterministic merge — projection owns its three, raw owns the rest

When the projection is current and covers the session, the adapter merges the two sources WITHOUT letting either overwrite the other's authoritative dimension: the projection's EXACT cost / tokens / failed-task values are never replaced by the raw scan's lower bounds, and the raw scan's canonical events / tasks / pending values are never replaced by a projection subset. The aggregate Partial is the raw scan's Partial — a partial raw dimension makes the whole rollup partial (its events / tasks / pending are honest lower bounds), and the projection-backed exact values ride along, never fabricated as current over an incomplete raw read.

Honest fallback — never missing data as exact zero

When the projection cannot be trusted for the session — the quality read fails, the state is `catching_up` / `unavailable`, the session window cannot be resolved, the retained horizon starts after the session opened (a retention gap), or the projection query itself fails — the adapter delegates to the raw CounterEnricher bounded scan and returns that rollup VERBATIM (its own Partial marking rides along: a truncated scan or an unreadable registry read stays an honest lower bound). The fallback is never silent: every delegation is Warn-logged with the reason, and the projection's freshness stays observable through the adapter's Quality accessor. If the raw fallback cannot provide a trustworthy result under its own contract (an unavailable substrate, an unreadable registry), its honest zero-plus-Partial result is preserved — availability is never fabricated.

Freshness is observable

"Current" is current-as-of-the-last-empty-read: a live runtime may persist an event a moment after that read (the projection moves to `catching_up` on its next advance), the same best-effort read-at-a-moment honesty the raw scan has. The adapter exposes the projection's Quality (state / watermark / retention) through Quality so wiring and operators can observe how fresh the served counters are.

Concurrent reuse (CLAUDE.md §5)

A constructed *ProjectionEnricher is immutable after NewProjectionEnricher: it holds only the store / quality / fallback / window / clock / logger references, each itself safe for concurrent reuse. Every Counters call's per-run state lives in its arguments and locals; the adapter reads nothing from itself for run-specific data.

func NewProjectionEnricher added in v1.28.0

func NewProjectionEnricher(deps ProjectionEnricherDeps) (*ProjectionEnricher, error)

NewProjectionEnricher builds the projection-backed Enricher adapter. Every dependency is mandatory — a nil Store / Quality / Fallback / Window fails loud with ErrMisconfigured rather than building an adapter that reports believable-but-false counters on one dimension (CLAUDE.md §5). The returned *ProjectionEnricher is immutable and safe for concurrent reuse.

func (*ProjectionEnricher) Counters added in v1.28.0

func (e *ProjectionEnricher) Counters(ctx context.Context, id identity.Identity, sessionID string) SessionCounters

Counters implements Enricher. It serves the session's counter rollup from the projection when the projection is current and covers the session (projection-backed cost / tokens / failed-task merged with the raw scan's canonical events / tasks / pending), and otherwise delegates to the raw bounded scan. It never turns missing projection data into an exact zero and never maps a projection subset onto a broader public counter.

func (*ProjectionEnricher) Quality added in v1.28.0

Quality exposes the rollup projection's operational freshness — the completeness state (`current` / `catching_up` / `unavailable`), the watermark, and the retained horizon — so the freshness of the counters the adapter serves is observable (a catching_up / unavailable projection is exactly when the adapter delegates to the raw fallback). Read-only and safe for concurrent use.

type ProjectionEnricherDeps added in v1.28.0

type ProjectionEnricherDeps struct {
	// Store is the rollup projection's query surface the adapter reads the
	// session's authoritative cost / tokens / failed-task measures from
	// (identity-scoped by the query filter — the session's own triple,
	// never cross-session bleed).
	Store rollups.Store
	// Quality reads the projection's freshness (state / watermark /
	// retention). The adapter serves exact projection-backed counters only
	// when Quality reports StateCurrent AND the retained horizon covers the
	// session.
	Quality ProjectionQuality
	// Fallback is the existing raw CounterEnricher bounded scan. It is the
	// canonical source of the dimensions the projection does not model —
	// total events emitted, total spawned tasks, and pending intervention —
	// on the projection-backed path, and the adapter delegates to it
	// VERBATIM whenever the projection cannot be trusted for the session.
	// Its result's own honest Partial marking rides along in both cases —
	// never replaced by fabricated zeros or fabricated availability.
	Fallback Enricher
	// Window resolves the session's lifetime for the retention-coverage
	// proof.
	Window SessionWindowFunc
	// Clock supplies the window's "now". Nil routes to time.Now().UTC().
	Clock func() time.Time
	// Logger receives Warn-level diagnostics when the adapter falls back or
	// a read cannot be taken (the degradation is NEVER silent). Nil routes
	// to slog.Default().
	Logger *slog.Logger
}

ProjectionEnricherDeps carries the ProjectionEnricher's mandatory dependencies. Every dependency is required — a nil one would silently disable a dimension or the coverage proof (the exact silent-absence class this adapter closes), so NewProjectionEnricher fails loud rather than build a half-blind adapter (CLAUDE.md §5).

type ProjectionQuality added in v1.28.0

type ProjectionQuality interface {
	Quality(ctx context.Context) (rollups.Quality, error)
}

ProjectionQuality reads the rollup projection's operational freshness — the read-only surface the rollup projector exposes (Quality on the projector: completeness state, watermark, retained horizon). Wired to the production projector in assembly; tests supply a controllable fake.

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

func NewService(projector Projector, opts ...Option) (*Service, error)

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

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

func (s *Service) HasEraser() bool

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

func (s *Service) HasTitleSetter() bool

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

Inspect implements the `sessions.inspect` method — the full per-session snapshot the Console Sessions detail view renders.

func (*Service) List

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

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 ANY of the reads behind this rollup could not
	// be taken in full: the bounded per-session event scan hit its bound
	// (ListWindow HasMore / Truncated), the windowed-read substrate was
	// unavailable, or a registry read (tasks / pauses) failed or could not
	// be scoped to the row. Every populated count is then an HONEST LOWER
	// BOUND, not exact, and the facet/sort layer MUST NOT treat a Partial
	// key as authoritative.
	//
	// The marker covers the registry reads deliberately: a failed
	// registry read leaves TasksCount / HasFailedTask /
	// HasPendingIntervention at their zero values, and a zero that means
	// "we could not look" must never be reported as a zero that means
	// "we looked and there were none".
	Partial bool
}

SessionCounters is the read-time counter rollup for one session — the six false-absence SessionRow counters plus the honest-partial marker.

type SessionWindowFunc added in v1.28.0

type SessionWindowFunc func(ctx context.Context, id identity.Identity, sessionID string) (openedAt, lastActivityAt time.Time, ok bool, err error)

SessionWindowFunc resolves a session's lifetime window so the adapter can prove the projection's retained horizon covers the session before trusting the rollup as exact. Production wiring supplies a session-registry-backed resolver (the snapshot's OpenedAt / LastSeen); tests supply a fake. ok=false means the window could not be resolved — the adapter then CANNOT prove coverage and delegates to the raw fallback rather than guessing (unproven coverage is never treated as exact).

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL