sessions

package
v1.21.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package sessions owns Harbor's session-lifecycle subsystem.

A Session is a longer-lived multi-turn conversation that contains many Runs. Identity for runtime concerns is the triple `(tenant, user, session)`; runs are scoped within sessions (RFC §6.9). The Session record itself is keyed in the StateStore at `Kind = "session.lifecycle"` with `RunID = ""` — sessions are session-scoped, not run-scoped.

Harbor ships a single concrete *Registry implementation that sits over the StateStore, codifying the typed-wrapper-over-generic StateStore contract. There is no driver pluralism at the session layer; driver pluralism lives at StateStore (in-mem / SQLite / Postgres already). Per AGENTS.md §4.4, optional-capability ceremony is forbidden when all V1 drivers (here: implementations) will implement everything.

Four lifetime invariants are load-bearing and pinned by tests:

  1. Identity captured immutably on Open — Touch / Close re-save the same identity from the existing record; mismatched ctx identity is rejected with ErrIdentityMismatch.
  2. Reopen-after-close allowed (RFC §6.9 amended) — a closed session (explicitly closed OR GC-reaped) is RE-ACTIVATED in place on the next Open / EnsureOpen: the immutable identity and OpenedAt are preserved, Closed/ClosedAt/ClosedReason are cleared, a LastReopenedAt is stamped, and the durable history left intact at close resumes. The ONE terminal exception is an ERASED session (`sessions.delete`, right-to-erasure): reopen fails loud with ErrReopenAfterErase — never a silent empty-start — INCLUDING after the erasure has fully converged and removed the lifecycle record (the not-found / fresh-create path is gated by isErased against a durable erasure tombstone). A still-closed session read via Touch keeps a loud read-only guard (ErrSessionClosed — Touch is not a reopen entry point).
  3. Cross-tenant SessionID reuse rejected — `SessionID=S` opened under Tenant A then attempted under Tenant B returns ErrSessionIDReuse, even though the StateStore key (which contains the full Quadruple) would not naturally collide.
  4. GC never reaps a session with a RUNNING task — enforced when the TaskRegistry-backed probe (TaskRunningProbe) is wired via WithGCPolicy, as both reference assemblies do. The no-op default (returns false, nil) exists only for registries constructed without task awareness.

Lifecycle events (`session.opened / .touched / .closed / .gc_reaped / .reopened`) land on the EventBus as `SafePayload` types — they're Harbor-internal markers with no secret-shaped fields by construction (RFC §6.13). Subscribers can extract typed fields directly, no redactor walk in between.

Index

Constants

View Source
const (
	EventTypeSessionOpened   events.EventType = "session.opened"
	EventTypeSessionTouched  events.EventType = "session.touched"
	EventTypeSessionClosed   events.EventType = "session.closed"
	EventTypeSessionGCReaped events.EventType = "session.gc_reaped"
	// EventTypeSessionReopened marks a closed session being re-activated in
	// place (RFC §6.9 amended): the next Open / EnsureOpen on a
	// Closed record clears the closed flags, preserves the immutable identity
	// and OpenedAt, and resumes the durable history. Content-free by
	// construction — carries only the SessionID, the reopen timestamp, and
	// the (operator/GC-set) prior close reason, NEVER user content or the
	// resumed conversation body. Consumers refetch the projection / windowed
	// history for the body.
	EventTypeSessionReopened events.EventType = "session.reopened"
	// EventTypeSessionErased marks a completed data-lifecycle erasure
	// (`sessions.delete`). It is the ONLY durable trace of the erasure —
	// a redacted, content-free record-of-fact emitted under the actor's
	// observability scope, NEVER under the erased session's own identity
	// (re-persisting under the erased triple would leave the session's
	// `state.history` non-empty — RFC §6.13 / §7).
	EventTypeSessionErased events.EventType = "session.erased"
	// EventTypeSessionTitleChanged marks a successful title change. It has TWO
	// producers: the `sessions.set_title` verb via SetTitle (a manual set OR
	// clear) and the internal auto-namer via SetTitleAuto (an auto set).
	// Content-free by construction: the title string NEVER rides this payload
	// — only the SessionID and the resulting TitleSource ("manual" / "auto" /
	// "" on clear). Consumers refetch the projection (`sessions.list` /
	// `sessions.inspect`) for the title text.
	EventTypeSessionTitleChanged events.EventType = "session.title_changed"
)

Session lifecycle event types. Each is registered with the events package's exhaustive registry via init() so Publish accepts them without ErrUnknownEventType. Subscribers can filter on these via events.Filter.Types.

View Source
const MaxSessionTitleLen = 200

MaxSessionTitleLen bounds a manual session title (runes, post-trim). A title exceeding this bound is rejected fail-loud with ErrInvalidTitle — never silently clamped (CLAUDE.md §13).

Variables

View Source
var (
	// ErrSessionClosed — Touch called on a SessionID whose record is
	// Closed. Touch is a read-only refresh and is NOT a reopen entry point:
	// a caller must go through Open / EnsureOpen (`start`) to re-activate a
	// closed session (RFC §6.9 amended). Kept as a loud guard so a
	// Touch on a closed record fails visibly rather than silently reviving.
	ErrSessionClosed = errors.New("sessions: session is closed — reopen via start before touching")
	// ErrReopenAfterErase — Open / EnsureOpen named a SessionID that was
	// permanently deleted by `sessions.delete` (right-to-erasure). An erased
	// session is the ONE terminal exception to reopen (RFC §6.9 amended /
	// §7): its data is gone, so reopen fails loud rather than silently
	// minting a fresh empty session. Detected via a durable erasure ledger
	// (in-flight erasure) OR tombstone (converged erasure) — so it fires even
	// after the erasure removed the lifecycle record. Callers map this to the
	// machine-branchable CodeSessionErased wire code (HTTP 409).
	ErrReopenAfterErase = errors.New("sessions: reopen-after-erase forbidden — the session was permanently deleted")
	// ErrSessionIDReuse — Open called with a SessionID already opened
	// under a different (tenant, user). Per RFC §6.9 ("reusing a session
	// ID across tenants/users is rejected").
	ErrSessionIDReuse = errors.New("sessions: SessionID reused across tenants/users")
	// ErrIdentityMismatch — Touch / Close called with a ctx Identity
	// that disagrees with the stored session's Identity. The triple is
	// captured immutably on Open; mid-flight identity swaps are bugs.
	ErrIdentityMismatch = errors.New("sessions: ctx identity mismatches stored session identity")
	// ErrSessionNotFound — Get / Touch / Close / Inspect targeting a
	// SessionID that has no record (or the record was Deleted).
	ErrSessionNotFound = errors.New("sessions: session not found")
	// ErrSessionAlreadyOpen — Open called twice with the same triple
	// AND SessionID without an intervening Close. Distinct from the reopen
	// path (which re-activates when Closed is true).
	ErrSessionAlreadyOpen = errors.New("sessions: session already open")
	// ErrRegistryClosed — any operation called after CloseRegistry.
	ErrRegistryClosed = errors.New("sessions: registry is closed")
	// ErrSessionRunning — Erase was refused because the target session
	// has a RUNNING task. Erasure mirrors the GC never-reap-running
	// invariant (RFC §6.9): a session with in-flight work is durable
	// execution state, not a cache entry, so it is refused fail-loud and
	// NO store is touched. The caller retries after the task finishes.
	ErrSessionRunning = errors.New("sessions: cannot erase a session with a running task")
)

Sentinel errors. Callers compare via errors.Is.

View Source
var ErrEraserMisconfigured = errors.New("sessions: CascadeEraser missing a mandatory dependency")

ErrEraserMisconfigured — NewCascadeEraser was called with a missing mandatory dependency. Fails closed (CLAUDE.md §5) rather than building an eraser that would nil-panic on the first erasure.

View Source
var ErrErasureRecordFailed = errors.New("sessions: erasure record-of-fact could not be durably completed")

ErrErasureRecordFailed — the cascade's destructive steps completed (in this attempt or an earlier converging one) but the durable record-of-fact could not be completed: a redactor refusal or a bus-publish failure at the final `session.erased` emit. Erase fails the WHOLE call loud rather than reporting success with a missing/incomplete audit trail. The session's scoped data IS gone; the durable erasure-ledger checkpoint (persisted before the irreversible StateStore.DeleteScope clear — see erasureLedgerRecord) survives, so a re-invoke converges: it skips straight to re-attempting the record + emit from the checkpointed cumulative counts, never re-running a destructive step.

View Source
var ErrInvalidTitle = errors.New("sessions: invalid title")

ErrInvalidTitle — SetTitle was called with a title that, after trimming leading/trailing whitespace, either exceeds MaxSessionTitleLen runes or contains a newline/control character. Titles are single-line, bounded, display strings.

View Source
var ErrManualTitle = errors.New("sessions: auto-naming refused — title is manual")

ErrManualTitle — SetTitleAuto was refused because the session's current TitleSource is TitleSourceManual. The internal auto-naming write path NEVER overwrites a human-set title: manual wins, structurally. A caller that sees this treats it as a benign skip (the run's outcome is unaffected), not a failure.

Functions

This section is empty.

Types

type AutoNamingState added in v1.12.0

type AutoNamingState struct {
	// TitleSource is the session's current title provenance — the auto-namer
	// never overwrites TitleSourceManual.
	TitleSource TitleSource
	// CurrentTitle is the session's current title (may be empty). The
	// re-naming path includes it in the digest so the model can refine an
	// existing auto title; it NEVER rides an event payload.
	CurrentTitle string
	// TurnCount / AutoNameCount / LastAutoNamedTurn are the session's naming
	// counters (see Session).
	TurnCount         int
	AutoNameCount     int
	LastAutoNamedTurn int
}

AutoNamingState is the read-side projection the auto-naming eligibility check consumes: the session's current title provenance plus its three naming counters. It is a lightweight read (no lifecycle mutation) so the terminal-boundary trigger can decide whether a title is due without a second whole-record write.

type CascadeEraser added in v1.7.0

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

CascadeEraser performs the ordered, fail-loud, idempotent session erasure cascade behind `sessions.delete`: it refuses fail-loud on a running task, then deletes the session's scoped Artifacts, Memory, and State (the kind-agnostic StateStore scope delete removes the durable session-lifecycle record + run-scoped trajectories + planner checkpoints + the durable event stream), clears the registry's in-memory catalogs, and completes with a redacted, content-free `session.erased` audit event under the actor's observability scope.

There is no ACID transaction across the three independent stores; the cascade is ordered and every per-store delete is idempotent, so a mid-cascade error surfaces loudly (never a partial silent success — CLAUDE.md §13) and the cascade is safe to re-invoke to convergence. The refuse-if-running pre-flight runs FIRST so a refusal touches nothing; the event-bus fence (see fenceSession) runs immediately after it and BEFORE any destructive step, and a present-but-failing fence capability fails the whole cascade loud right there — the same retry-safe posture as a mid-cascade store error, just earlier.

The audit record is part of the erasure's success criteria

Two follow-ups (issues #409/#410) hardened this cascade's audit trail on top of the ordering + fencing shape above. Previously, the final `session.erased` emit was best-effort AFTER the destructive steps completed: a transient bus/redactor failure lost the ONLY audit record while Erase still reported success, and — because the destructive steps (specifically StateStore.DeleteScope) had already removed the session-lifecycle record — a re-invoke returned ErrSessionNotFound, leaving no second chance to emit. Separately, a mid-cascade retry re-ran idempotent deletes that found fewer records the second time, so the reported counts reflected only the final converging attempt.

The BINDING ordering invariant: at no point may (data irrevocably gone) ∧ (no durable audit record) ∧ (success returned) hold — nor the inverse (record says erased, data present, success returned). The chosen mechanism is to persist a durable compliance checkpoint (erasureLedgerRecord, via the StateStore the cascade already drives) BEFORE each destructive step's result would otherwise become unrecoverable, rather than bounded-retrying the emit in-call. Concretely:

  1. The ledger checkpoint is persisted under the actor's observability scope (ledgerScope — same reserved (tenant, user, <erasure-audit>) slot the final event publishes under), NOT the erased triple, so it survives StateStore.DeleteScope's clear of the erased session's own scope. It is tenant/user-scoped OPERATOR AUDIT DATA about a deleted session — never user content — addressing the phase plan's identity-scoping risk note head-on.
  2. The ledger is checkpointed immediately after EVERY destructive step (artifacts, memory, state), accumulating each step's own count onto whatever a PRIOR interrupted attempt already contributed (loaded at the top of Erase). This is what makes counts cumulative across converging attempts (#410): a step whose data is already gone on a retry contributes 0 locally, but the ledger already holds the earlier attempt's non-zero contribution. The ledger carries the session lifecycle's OpenedAt stamp so accumulation NEVER crosses a session-id reuse boundary: when the target session still exists (a fresh attempt, not a converging retry) and a leftover ledger's stamp mismatches the live session's OpenedAt, the leftover belongs to an ABANDONED prior lifecycle of the reused id — it is discarded (with a loud Warn) and the cascade starts from zero, so a reused session id's counts reflect only its own lifecycle. Discarding necessarily forfeits the abandoned lifecycle's never-emitted record-of-fact: the caller was told loudly (ErrErasureRecordFailed) to re-invoke and instead reopened the id — inflating the NEW lifecycle's compliance counts to preserve a stale checkpoint would be the worse corruption.
  3. Because the ledger holds the complete, accurate counts BEFORE StateStore.DeleteScope ever runs (steps 1-2 checkpoint before step 3), the compliance record is durably persisted strictly before the irreversible clear — satisfying the first half of the invariant. The final record-of-fact emit (completeErasure/emitErased) reads ONLY from the ledger, never recomputing counts, so it can't drift.
  4. A re-invoke after the destructive steps completed but the final emit failed finds the session already gone (ErrSessionNotFound from the registry pre-flight) AND a still-present ledger checkpoint — recognized as a CONVERGING retry: it skips every destructive step (nothing left to do) and goes straight to completeErasure, which re-attempts the record + emit using the ledger's cumulative counts. Only a successful emit clears the ledger, so an emit failure keeps the session "re-invokable" without ever touching a store again.
  5. A redactor refusal or a bus-publish failure at the final emit both fail Erase loud with the wrapped sentinel ErrErasureRecordFailed (no `Error`-log-and-continue) — the same loud path, because both mean the durable record-of-fact did not land.
  6. The final emit is IDEMPOTENT per (session, lifecycle): before publishing, completeErasure checks the observability scope's retained history (when the bus supports events.HistoryReplayer) for a `session.erased` record already carrying this session id + lifecycle stamp, and on a hit skips the publish — converging silently to success and still clearing the ledger. This closes the duplicate-emit window when the publish succeeded but the ledger cleanup failed and the caller re-invoked. The check is capability-gated and bounded (see recordAlreadyEmitted): when it cannot verify, it emits — a duplicate compliance record is the acceptable failure direction, a lost one never is.
  7. Concurrent Erase calls for the SAME session are serialized by a striped IN-PROCESS lock (lockSession) so exactly one goroutine in this process ever runs the cascade for a given session at a time: the loser blocks until the winner's cascade (through ledger cleanup) is fully done, then sees the genuine ErrSessionNotFound with no ledger — "one wins, one gets the not-found path, never a double event." HONESTY NOTE — the lock is per-process only: two runtime REPLICAS racing sessions.delete for the same session are NOT serialized (their ledger checkpoints are last-writer-wins StateStore saves, and both may run destructive steps — each of which is idempotent, so the data outcome converges). The cross-replica residuals are count skew on the racing attempts and a possible duplicate emit where the item-6 history check races the other replica's publish; never a lost record, never resurrected data. Cross-replica serialization would need a StateStore CAS/lease primitive — the same recorded limitation as the agent-config registry's per-process owner locks.

Documented residual gap (accepted, not silently dropped): there is no ACID transaction spanning a destructive store (artifacts/memory/state) and the StateStore the ledger checkpoint itself is persisted through — a generalized transactional-outbox subsystem is an explicit non-goal of this design. A failure in the narrow window between a destructive step succeeding and its OWN checkpoint save committing means that step's contribution is not durably recorded; a converging retry's local count for that step is by then 0 (already deleted), so the final total under-counts by exactly that step's amount. This is strictly narrower than the earlier whole-cascade gap it replaces (a failure ANYWHERE previously lost the whole attempt's counts) and is pinned by TestCascadeEraser_LedgerSaveFailure_LoudAndRetrySafe rather than left silently untested. It never affects the fail-loud / no-lost-record invariant (CLAUDE.md §13) — only count exactness on this specific interleaving.

A constructed *CascadeEraser is immutable after construction and safe to share across N concurrent goroutines: every method's per-call state lives in the call's arguments and locals, the shared stores + registry are each independently concurrency-safe, and the fixed-size striped lock array (eraseLocks) is the one internally-synchronized mutable field — it never grows with the number of distinct sessions ever erased.

func NewCascadeEraser added in v1.7.0

func NewCascadeEraser(deps CascadeEraserDeps) (*CascadeEraser, error)

NewCascadeEraser builds the session-erasure cascade orchestrator. Registry / State / Memory / Artifacts / Bus are mandatory — a nil fails loud with a wrapped ErrEraserMisconfigured. The returned *CascadeEraser is immutable after construction and safe for concurrent use by N goroutines.

func (*CascadeEraser) Erase added in v1.7.0

Erase runs the full erasure cascade for the verified identity. The identity is the caller's own verified `(tenant, user, session)` — the own-session-only scope contract is enforced at the wire / service edge before Erase is reached. Order (RFC §6.9, hardened with the durable record-of-fact ordering + cumulative counts described above):

  1. lock: serialize concurrent Erase calls for this exact session (lockSession) so only one goroutine ever runs the steps below for it at a time; load any durable erasure-ledger checkpoint a PRIOR interrupted attempt left behind.
  2. refuse-if-running: load+verify the session record under id and probe the running-task seam. A refusal touches NO store — UNLESS a ledger checkpoint already exists, in which case ErrSessionNotFound here means the destructive steps already fully completed on a prior attempt and only the final record-of-fact remains: Erase converges via completeErasure without touching artifacts / memory / state again. When the session EXISTS, the ledger's lifecycle stamp (the session's OpenedAt) is verified: a leftover ledger from an abandoned prior lifecycle of a reused session id is discarded with a loud Warn so counts never inflate across a reuse boundary. 1a. erase-intent checkpoint: persist the ledger (empty-but-stamped, or the accumulated counts on a retry) through the registry's r.mu BEFORE the fence, so isErased reports true from the first destructive instant and a concurrent reopen cannot re-activate the record and lift the fence in the gap before the first destructive checkpoint (the fence-lift race; see the inline note).
  3. fence: mark the triple erased on the event bus, BEFORE any destructive step. A present-but-failing Fence capability fails the WHOLE erasure loud here — nothing has been touched yet, so this is retry-safe. A bus that does not implement the capability at all is a logged downgrade, not an error (see fenceSession).
  4. artifacts: enumerate the session's artifacts and delete each, then durably checkpoint the cumulative count (issue #410).
  5. memory: flush the session's memory to a clean state, then checkpoint.
  6. state: kind-agnostic scope delete (the session-lifecycle record, run-scoped trajectories, planner checkpoints, and the durable event stream all live under the triple and all go) — THE irreversible clear — then checkpoint the final cumulative count. Because steps 3-5 each checkpoint before the NEXT step runs, the ledger is always durably complete strictly before this step's clear takes effect ("record before the irreversible clear" ordering).
  7. complete: clear the registry's in-memory catalogs, build the redacted content-free `session.erased` payload from the ledger's cumulative counts, publish it (skipping idempotently when a record for this session+lifecycle already exists — recordAlreadyEmitted), and — only once the record durably exists — remove the ledger checkpoint (completeErasure).

Returns ErrSessionRunning (refused — 409), ErrSessionNotFound (absent under the caller's identity AND no pending ledger — 404), ErrErasureRecordFailed (destructive steps done, record incomplete — re-invokable), or a wrapped store/fence error (loud, retry-safe: every step through state.DeleteScope is idempotent, and the fence step (2) runs before step 3 ever touches a store). The response carries non-sensitive deletion telemetry only.

type CascadeEraserDeps added in v1.7.0

type CascadeEraserDeps struct {
	Registry  *Registry
	State     state.StateStore
	Memory    memory.MemoryStore
	Artifacts artifacts.ArtifactStore
	Bus       events.EventBus
	Redactor  audit.Redactor
	Clock     Clock
	Logger    *slog.Logger
}

CascadeEraserDeps bundles the seams the CascadeEraser drives. Registry, State, Memory, Artifacts, and Bus are mandatory; Redactor, Clock, and Logger are optional.

type Clock

type Clock interface {
	Now() time.Time
}

Clock abstracts time so GC tests are deterministic without time.Sleep. Production code uses realClock; tests pass a fakeClock.

The interface intentionally returns time.Time directly (not a monotonic count) so GC's wall-clock math is identical between production and test paths.

type GCPolicy

type GCPolicy struct {
	IdleTTL       time.Duration
	HardCap       time.Duration
	SweepInterval time.Duration
	RunningProbe  RunningProbe
}

GCPolicy bundles the GC sweeper's tunables. Defaults match RFC §6.9: IdleTTL 24h, HardCap 720h (30 days), SweepInterval 15m. The RunningProbe should be TaskRunningProbe wherever a TaskRegistry is in scope; default is the no-op.

type Option

type Option func(*Registry)

Option configures a Registry at construction. Only WithClock is exported; production code does not touch it. Tests use a fake clock to drive GC deterministically.

func WithClock

func WithClock(c Clock) Option

WithClock injects a custom Clock. Production code uses realClock; the test suite uses a controllable clock to exercise hard-cap GC without time.Sleep (per AGENTS.md §11).

func WithGCPolicy

func WithGCPolicy(p GCPolicy) Option

WithGCPolicy injects an explicit GCPolicy at construction. When omitted, the GCPolicy is built from the SessionsConfig defaults + the no-op RunningProbe. Production wiring (cmd/harbor and harbortest/devstack) passes a policy whose RunningProbe is TaskRunningProbe so GC honors RFC §6.9.

type Registry

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

Registry is the StateStore-backed implementation of SessionRegistry. It is the single concrete impl in V1 — driver pluralism lives at the StateStore layer.

Concurrency model:

  • StateStore writes go through `state.StateStore.Save` which is itself concurrent-safe per the concurrent-reuse contract.
  • The cross-tenant SessionID-uniqueness map is guarded by mu.
  • The sweeper goroutine's lifecycle is owned by `done` + `wg`.

func New

func New(store state.StateStore, cfg config.SessionsConfig, bus events.EventBus, opts ...Option) (*Registry, error)

New constructs a Registry. The store and bus are required; cfg supplies the GC tunables (defaults applied if zero); opts can override the Clock or GCPolicy.

The sweeper goroutine starts immediately and runs at gcPolicy.SweepInterval until CloseRegistry is called. The probe defaults to no-op; assemblies that own a TaskRegistry MUST pass TaskRunningProbe via WithGCPolicy (both reference assemblies do) so GC never reaps a session with a RUNNING task.

func (*Registry) AutoNamingState added in v1.12.0

func (r *Registry) AutoNamingState(ctx context.Context, id string, ident identity.Identity) (AutoNamingState, error)

AutoNamingState reads the session's title provenance + naming counters for the terminal-boundary eligibility check. It is a read-only load (no lifecycle mutation, no lock held across the read) under the run's verified triple; an unknown / erased id returns ErrSessionNotFound, a stored-identity mismatch returns ErrIdentityMismatch.

func (*Registry) Close

func (r *Registry) Close(ctx context.Context, id string, reason string) error

Close marks the session Closed and emits session.closed. Idempotent: closing an already-closed session is a no-op AND preserves the original ClosedReason. The load→mutate→save runs through mutateSession (one r.mu critical section — this previously loaded OUTSIDE the lock, leaving a lost-update window where a concurrent SetTitle / Touch could re-persist Closed=false after Close returned).

func (*Registry) CloseRegistry

func (r *Registry) CloseRegistry(_ context.Context) error

CloseRegistry cancels the sweeper goroutine and joins it. Idempotent. Subsequent operations return ErrRegistryClosed.

func (*Registry) EnsureOpen added in v1.2.0

func (r *Registry) EnsureOpen(ctx context.Context, ident identity.Identity) (*Session, error)

EnsureOpen is the create-on-first-use entry point. It returns the live session for `ident`, creating it if no record exists. The session id is `ident.SessionID` (the per-request session the client chose); the (tenant, user) come from the verified connection token.

Semantics:

  • No record yet → Open it (create) and return the fresh session (UNLESS the id was erased — ErrReopenAfterErase, never a silent fresh empty session on a converged erasure).
  • Open record at this exact triple → no-op, return the existing session (a second turn in the same conversation is not an error).
  • Closed record at this triple → RE-ACTIVATE it in place and return the resumed session (RFC §6.9 amended: a GC-reaped or operator-closed session reopens with its durable history intact). An ERASED session is the terminal exception → ErrReopenAfterErase.

EnsureOpen is identity-mandatory: an incomplete triple fails closed. It is the seam the Protocol ControlSurface calls on `start` so the first turn of a brand-new conversation materialises the session row the Console's sessions.list reads.

func (*Registry) Erase added in v1.7.0

func (r *Registry) Erase(ctx context.Context, id string) error

Erase is the registry-side pre-flight + in-memory clear for a session erasure (`sessions.delete`). It runs the fail-loud preconditions that MUST hold before any store is touched — load+verify the record under the caller's verified ctx identity, then probe the RunningProbe seam — and, when they pass, clears the in-memory open-sessions / id-index map entry and the per-(tenant, user) discovery catalog.

Erase performs NO durable record delete of its own: the durable `session.lifecycle` record is itself a StateStore record under the triple, removed by the kind-agnostic StateStore.DeleteScope step of the cascade. Calling Erase standalone leaves the durable record in place (the caller is expected to run DeleteScope); the erasure orchestrator composes the two via the unexported preflightErase / clearErased halves so the probe runs FIRST (a refusal touches nothing) and the in-memory clear runs LAST (after DeleteScope).

Identity-mandatory: the ctx identity scopes the read, and its SessionID must equal id. Returns ErrSessionNotFound when no record is visible to the caller's identity, ErrSessionRunning when the probe reports a RUNNING task (no store touched), ErrIdentityMismatch when the stored identity disagrees with the caller's.

func (*Registry) GC

func (r *Registry) GC(ctx context.Context, policy GCPolicy) (int, error)

GC performs a single sweep pass. For each currently-open session:

  • if RunningProbe returns true: skip (per RFC §6.9 "GC never reaps a session with a RUNNING task").
  • else if LastSeen + IdleTTL < clock.Now(): close with reason "gc:idle".
  • else if max(OpenedAt, LastReopenedAt) + HardCap < clock.Now(): close with reason "gc:hard_cap" (the hard cap wins over recent Touch, but a reopen restarts the cap clock so a resumed conversation survives).

Returns the number of sessions reaped and the first probe / close error encountered (the sweep continues past errors so a single bad session doesn't block the rest).

func (*Registry) Get

func (r *Registry) Get(ctx context.Context, id string) (*Session, error)

Get loads the session with `id` for the identity in ctx. Returns ErrSessionNotFound when the record is absent. Identity-mandatory: the ctx Identity is used to scope the StateStore read; cross-tenant access is prevented because the StateStore key contains the full triple.

func (*Registry) Inspect

func (r *Registry) Inspect(ctx context.Context, id string) (*SessionSnapshot, error)

Inspect returns a SessionSnapshot. Running is derived from the configured RunningProbe at inspection time.

func (*Registry) ListSnapshots

func (r *Registry) ListSnapshots(ctx context.Context, f SessionListFilter) ([]SessionSnapshot, error)

ListSnapshots implements sessions.SessionLister — the `search.sessions` read-side projection. Returns snapshots for every session the registry has seen (open OR closed) matching the filter.

The registry's in-memory `idIndex` is the catalog of every SessionID that has been Opened during this registry's lifetime. The snapshot is built by Loading each matching session from the StateStore so the Closed / ClosedAt / LastSeen fields are current; Running is derived from the GCPolicy RunningProbe at inspection time (mirroring Inspect's contract).

The caller (the search subsystem) is responsible for the auth scope gate — ListSnapshots does NOT re-check scope. It DOES validate that every supplied `TenantIDs` / `UserIDs` / `SessionIDs` entry is non-empty (a no-op for empty filters).

Concurrent reuse: ListSnapshots only reads `idIndex` / `openSessions` under the registry's mutex; no per-call state lives on `*Registry`. One Registry serves N concurrent ListSnapshots safely.

func (*Registry) OldestRetainedAt added in v1.14.0

func (r *Registry) OldestRetainedAt(ctx context.Context) (time.Time, bool, error)

OldestRetainedAt reports the OBSERVED oldest session-open instant across the WHOLE retained session set — every (tenant, user, session) this registry has seen (open OR closed-but-retained) — as the identity-free runtime-wide retention horizon a fleet-observe caller reads on `runtime.health`. It is the runtime-wide analogue of events.RetentionReporter, discovered by type assertion at the posture wiring seam (no `Supports*` capability protocol — CLAUDE.md §4.4). The returned instant carries no per-session content — only the bare oldest OpenedAt — so it is safe to expose runtime-wide, never a cross-tenant enumeration.

"Oldest RETAINED", not "oldest ever": the registry's idle GC reaps old sessions, so the horizon tracks what is still held. Returns (zero, false, nil) when the registry retains no sessions (present is false), and (zero, false, ErrRegistryClosed) after CloseRegistry.

The catalog scanned is the in-memory `idIndex` (every SessionID Opened during this process's lifetime); a session created by a PRIOR process whose (tenant, user) has not been hydrated is not counted — the same documented lazy-hydration gap ListSnapshots carries for an unscoped fleet read (a store-level List is post-V1). A per-session load error is skipped rather than failing the whole horizon (best-effort observability, never a load-bearing path).

func (*Registry) Open

func (r *Registry) Open(ctx context.Context, id string, ident identity.Identity) (*Session, error)

Open creates a new session record, captures the identity triple immutably, and emits session.opened. A closed record is RE-ACTIVATED in place (RFC §6.9 amended): its identity + OpenedAt are preserved, the closed flags cleared, LastReopenedAt stamped, and a content-free session.reopened emitted; an ERASED session instead fails loud with ErrReopenAfterErase (the terminal exception, detected via the erasure ledger/tombstone even after the record is gone). Cross-tenant SessionID reuse is rejected with ErrSessionIDReuse; same-triple double-open is rejected with ErrSessionAlreadyOpen.

func (*Registry) RecordCompletedTurn added in v1.12.0

func (r *Registry) RecordCompletedTurn(ctx context.Context, id string, ident identity.Identity) (int, error)

RecordCompletedTurn increments the session's TurnCount and returns the new value. The run loop's terminal boundary calls it once per completed run WHEN a naming policy is active — never otherwise, so a naming-off runtime writes nothing (the opt-in invariant). The load→bump→save runs through mutateSession (one r.mu critical section, shared with Touch / Close / SetTitle / SetTitleAuto / the GC reap and the eraser's deleteScopeSerialized) so a concurrent write can never lose the increment or resurrect an erased record.

`ident` is the run's verified (tenant, user, session). The target session is loaded under that exact triple; a session belonging to a different (tenant, user, session) is not found at that StateStore key (ErrSessionNotFound). A stored-identity mismatch (defence-in-depth) returns ErrIdentityMismatch. Renaming/counting a CLOSED session is allowed (a run can complete against a session the operator closed mid-flight); an ERASED session is ErrSessionNotFound.

func (*Registry) SetTitle added in v1.12.0

func (r *Registry) SetTitle(ctx context.Context, id string, ident identity.Identity, title string) error

SetTitle sets or clears the manual title of session `id` for the CALLER's verified `(tenant, user)` (`ident`). Unlike Touch/Close/ Inspect, the target `id` is NOT required to equal `ident.SessionID` — the write scope is `(tenant, user)`, matching `sessions.list`, so a caller may rename a sibling session. `ident` still carries a full triple (identity.Validate requires all three components) — its SessionID is the caller's OWN connecting session and is used only for identity validation, never as the load key.

Semantics: the title is trimmed; an empty-after-trim value clears Title, resets TitleSource to TitleSourceUnset, AND zeroes the auto-naming counters (AutoNameCount / LastAutoNamedTurn) in the same save — a clear starts a NEW auto-naming arming cycle, so a re-arm fires again (the cap is per-cycle, not per-session-lifetime). A non-empty value is validated (MaxSessionTitleLen, no newline/control characters — ErrInvalidTitle on violation, never a silent clamp per CLAUDE.md §13) and stored with TitleSource = TitleSourceManual. A write that changes nothing (clearing an already-unset title, or an identical manual re-set) is a no-op: nothing is persisted and no session.title_changed event is emitted. The target session is loaded under the caller's own (tenant, user) plus the target id — a session belonging to a DIFFERENT (tenant, user) is simply not found at that StateStore key, so a cross-user/cross-tenant rename attempt surfaces as the same ErrSessionNotFound a genuinely unknown id would (existence is never revealed across identities). Renaming a CLOSED session is allowed (title is metadata on a historical conversation, not a lifecycle mutation); renaming an ERASED session is ErrSessionNotFound (DeleteScope already removed the record).

func (*Registry) SetTitleAuto added in v1.12.0

func (r *Registry) SetTitleAuto(ctx context.Context, id string, ident identity.Identity, title string) error

SetTitleAuto sets the session's title from the internal auto-namer. It REFUSES with ErrManualTitle when the current TitleSource is TitleSourceManual — manual wins, structurally, so a human's title is never overwritten by the runtime. On success it stores the (defensively re-clamped) title with TitleSource = TitleSourceAuto and bumps AutoNameCount + LastAutoNamedTurn in the SAME record save (never a torn two-write update), then publishes a content-free session.title_changed (source=auto).

`title` is expected to be already clamped by the caller (the trigger) to the naming policy's max-title-len; the registry re-clamps defensively to MaxSessionTitleLen runes on a single line and rejects an empty-after-trim title with ErrInvalidTitle (the auto path never intentionally clears — an empty candidate is the caller's `empty_title` skip, handled before this call). The whole read→refuse/mutate→save runs through mutateSession (the one serialized whole-record path).

func (*Registry) Touch

func (r *Registry) Touch(ctx context.Context, id string) error

Touch updates LastSeen and re-saves. Identity-mandatory; ctx Identity is compared against the stored Identity, mismatch returns ErrIdentityMismatch. Touch on a Closed session returns ErrSessionClosed (Touch is a read-only refresh, NOT a reopen entry point — a caller must go through Open / EnsureOpen / `start` to re-activate a closed session). The load→mutate→save runs through mutateSession (one r.mu critical section) so a concurrent Close / SetTitle / GC reap can never lose this write — or have this write resurrect theirs.

type RunningProbe

type RunningProbe func(ctx context.Context, q identity.Quadruple) (bool, error)

RunningProbe is the seam the GC sweeper consults so it can honor "never reap a session with a RUNNING task" (RFC §6.9). The TaskRegistry-backed implementation is TaskRunningProbe; both reference assemblies wire it via WithGCPolicy. A nil probe is treated as the no-op default (returns false, nil) — only for registries constructed without task awareness.

func TaskRunningProbe added in v1.3.0

func TaskRunningProbe(reg tasks.TaskRegistry) RunningProbe

TaskRunningProbe adapts a tasks.TaskRegistry into the RunningProbe seam so the GC sweeper can enforce RFC §6.9's "GC never reaps a session with a RUNNING task" invariant. The probe lists the session's tasks filtered to tasks.StatusRunning (the exact status the RFC names — PENDING / PAUSED tasks do not block GC) and reports whether any exist.

Wiring: both reference assemblies (cmd/harbor `bootDevStack` and harbortest/devstack `Assemble`) pass this probe via sessions.New(..., WithGCPolicy(GCPolicy{RunningProbe: ...})). A registry constructed without it falls back to the no-op default, which reports no running tasks — sessions with in-flight work become reapable, so assemblies that own a TaskRegistry MUST wire this adapter.

The import direction is sound by construction: internal/tasks does not import internal/sessions (verified at the time of authoring), so the adapter lives here, next to the RunningProbe type it satisfies.

type Session

type Session struct {
	ID           string
	Identity     identity.Identity
	OpenedAt     time.Time
	LastSeen     time.Time
	Closed       bool
	ClosedAt     time.Time
	ClosedReason string
	// LastReopenedAt is the timestamp of the most recent reopen (Open /
	// EnsureOpen re-activating a Closed record). Zero on a session that was
	// never reopened. Additive JSON field — an older persisted record decodes
	// with it zero-valued, so no migration is needed. It exists as a SEPARATE
	// stamp from OpenedAt because OpenedAt must NOT be refreshed on reopen:
	// OpenedAt is the erasure cascade's lifecycle discriminator
	// (erasureLedgerRecord.SessionOpenedAt) and mutating it would corrupt
	// erasure reuse-boundary discrimination. The GC hard cap is measured from
	// max(OpenedAt, LastReopenedAt), so a reopened conversation restarts its
	// hard-cap clock from the reopen (resume-forever) without disturbing the
	// erasure discriminator.
	LastReopenedAt time.Time
	Limits         SessionLimits
	Context        map[string]any
	Title          string
	TitleSource    TitleSource
	// TurnCount / AutoNameCount / LastAutoNamedTurn are the auto-naming
	// counters. They are additive JSON fields: an older persisted record
	// (written before auto-naming existed) decodes with all three
	// zero-valued. They are written ONLY when a naming policy is active for
	// the run (a config-free runtime never touches them), so a naming-off
	// fleet carries no per-run write amplification and the counters read
	// zero for every session. Written exclusively through
	// RecordCompletedTurn (bumps TurnCount) and SetTitleAuto (bumps
	// AutoNameCount + LastAutoNamedTurn alongside the title, in ONE record
	// save).
	//
	// TurnCount counts runs completed while a naming policy was active —
	// counting starts at policy enablement, so enabling naming mid-session
	// counts turns from enablement, not from session open (a documented
	// consequence of writing counters only when the policy is active).
	// AutoNameCount is the number of auto-naming LLM calls that produced a
	// title. LastAutoNamedTurn is the TurnCount at which the most recent
	// auto-naming landed (the re-naming cadence anchor).
	TurnCount         int
	AutoNameCount     int
	LastAutoNamedTurn int
}

Session is the persisted lifecycle record for one session. The Identity field carries the triple captured on Open and is immutable afterwards. Closed transitions to true on Close; ClosedAt stays zero while Closed is false.

Title / TitleSource are additive fields: an older persisted session record (one written before these fields existed) decodes with both zero-valued (""), which is exactly TitleSourceUnset — no migration needed. Title has two writers: SetTitle (the sessions.set_title verb — manual set/clear, with trim + length/control-character validation, and an empty value clears the title and resets the auto-naming counters) and SetTitleAuto (the internal auto-namer — writes TitleSource="auto", refusing to overwrite a manual title). Title is user-derived free text and MUST NEVER be copied into an event/log/audit payload (CLAUDE.md §7 rule 7 — content-free posture) — only SessionID + TitleSource ride SessionTitleChangedPayload.

Limits and Context are reserved slots — later phases will populate Limits with the cost / token ceilings and Context with the (version, hash, llm/tool ctx, memory, artifacts) quintuple sketched in RFC §6.9. round-trips both fields through marshal/unmarshal but applies no validation.

type SessionClosedPayload

type SessionClosedPayload struct {
	events.SafeSealed
	SessionID string
	ClosedAt  int64 // unix nanoseconds
	Reason    string
}

SessionClosedPayload reports a Close. Carries the SessionID, the ClosedAt timestamp, and the operator-provided Reason. Reason is a short caller-controlled string — callers MUST NOT pass tool args, raw user input, or any secret-shaped material; the bus does not re-redact SafePayload types.

type SessionErasedPayload added in v1.7.0

type SessionErasedPayload struct {
	events.SafeSealed
	// SessionID is the session that was erased.
	SessionID string
	// StateRecordsDeleted is the number of StateStore records removed by
	// the kind-agnostic scope delete. Cumulative across converging
	// attempts.
	StateRecordsDeleted int
	// ArtifactsDeleted is the number of artifacts removed. Cumulative
	// across converging attempts.
	ArtifactsDeleted int
	// MemoryPurged is true when the session's memory was flushed clean.
	MemoryPurged bool
	// ErasedAt is the erasure timestamp (unix nanoseconds).
	ErasedAt int64
}

SessionErasedPayload reports a completed session erasure. It carries the erased SessionID plus the per-store deletion counts and the erasure timestamp — non-sensitive telemetry only, NO user content. The erased session id is a payload FIELD (not the event's Identity): the event is published under the actor's observability scope so it never re-persists durable state under the erased triple. SafePayload by construction — every field is a bounded id, count, or timestamp.

This is the durable record-of-fact for a right-to-erasure operation, and publishing it is part of `sessions.delete`'s SUCCESS CRITERIA rather than a best-effort afterthought: Erase only ever returns success once this payload has been redacted and durably published. The count fields are CUMULATIVE across any converging retries the cascade needed (see internal/sessions.CascadeEraser's package godoc) — the true total actually removed, never just the last attempt's own count.

type SessionGCReapedPayload

type SessionGCReapedPayload struct {
	events.SafeSealed
	SessionID string
	ReapedAt  int64 // unix nanoseconds
	Reason    string
}

SessionGCReapedPayload reports a GC sweep reaping. Reason is one of "gc:idle" or "gc:hard_cap"; the same string is also stored in Session.ClosedReason. SafePayload by construction.

type SessionLimits

type SessionLimits struct {
}

SessionLimits is reserved for later phases (cost ceilings, tool catalog). Empty at V1; round-trips through marshal.

type SessionListFilter

type SessionListFilter struct {
	TenantIDs     []string
	UserIDs       []string
	SessionIDs    []string
	SinceLastSeen time.Time
	UntilLastSeen time.Time
	IncludeClosed bool
}

SessionListFilter narrows ListSnapshots's result set. All fields are wildcards when empty. SinceLastSeen / UntilLastSeen filter by the LastSeen timestamp; zero means "no bound."

type SessionLister

type SessionLister interface {
	// ListSnapshots returns session snapshots that match the filter,
	// scoped to the requested tenants. Empty `TenantIDs` matches every
	// tenant the registry has seen (the search subsystem gates this
	// on the caller's auth.ScopeAdmin claim — the registry does NOT
	// re-check scope). Empty `UserIDs` / `SessionIDs` are wildcards.
	ListSnapshots(ctx context.Context, f SessionListFilter) ([]SessionSnapshot, error)
}

SessionLister is the narrow read-side capability the `search.sessions` Searcher consumes. The triple `(tenant, user, session)` is the load-bearing isolation key (CLAUDE.md §6); the listing is server-enforced per the supplied SessionListFilter. The returned snapshots include both currently-open and previously-closed sessions — search wants the union.

Intentionally NOT on the SessionRegistry interface: the lister is a projection over the in-memory open-session index plus the StateStore. Concrete `*Registry` implements it; future drivers add it when their backing store gains a `list` capability (StateStore List is post-V1).

type SessionOpenedPayload

type SessionOpenedPayload struct {
	events.SafeSealed
	SessionID string
	OpenedAt  int64 // unix nanoseconds; identity-of-record across drivers
}

SessionOpenedPayload reports a successful Open. Carries the SessionID and the OpenedAt timestamp; the identity triple lives on the Event itself, so it is intentionally NOT duplicated here.

SafePayload by construction — no secret-shaped fields.

type SessionRegistry

type SessionRegistry interface {
	Open(ctx context.Context, id string, ident identity.Identity) (*Session, error)
	// EnsureOpen is the create-on-first-use entry point: it
	// returns the live session for ident, creating it if absent,
	// no-opping if already open, and RE-ACTIVATING it if closed (RFC §6.9
	// amended). An ERASED session is the terminal exception:
	// EnsureOpen fails loud with ErrReopenAfterErase. See the concrete impl
	// for full semantics.
	EnsureOpen(ctx context.Context, ident identity.Identity) (*Session, error)
	Get(ctx context.Context, id string) (*Session, error)
	Touch(ctx context.Context, id string) error
	Close(ctx context.Context, id string, reason string) error
	Inspect(ctx context.Context, id string) (*SessionSnapshot, error)
	GC(ctx context.Context, policy GCPolicy) (int, error)

	// SetTitle sets or clears the manual title of session `id`. `ident`
	// is the CALLER's verified (tenant, user, session) — id MAY name a
	// sibling session of the same (tenant, user) (the write scope is
	// (tenant, user), matching sessions.list); it is not
	// required to equal ident.SessionID. Semantics: trims whitespace;
	// an empty-after-trim title clears Title and resets TitleSource to
	// TitleSourceUnset; a non-empty title is validated (MaxSessionTitleLen,
	// no newline/control characters — ErrInvalidTitle, never a silent
	// clamp) and stored with TitleSource = TitleSourceManual. Unknown id
	// (or an id belonging to a different (tenant, user)) returns
	// ErrSessionNotFound; a stored-identity (tenant, user) mismatch
	// (defence-in-depth) returns ErrIdentityMismatch. Renaming a CLOSED
	// session is allowed; renaming an ERASED session is
	// ErrSessionNotFound (the record is gone).
	SetTitle(ctx context.Context, id string, ident identity.Identity, title string) error

	// RecordCompletedTurn increments the session's TurnCount and returns the
	// new value. It is called from the run loop's terminal boundary ONLY when
	// a naming policy is active for the run — a naming-off runtime never calls
	// it, so it writes nothing for the naming-off fleet. `ident` is the run's
	// verified (tenant, user, session); a stored-identity mismatch returns
	// ErrIdentityMismatch, an unknown / erased id returns ErrSessionNotFound.
	// The load→bump→save runs through the serialized whole-record write path
	// (never a torn two-write update).
	RecordCompletedTurn(ctx context.Context, id string, ident identity.Identity) (int, error)

	// SetTitleAuto sets the session's title from the internal auto-namer and
	// bumps AutoNameCount + LastAutoNamedTurn in the SAME record save. It
	// REFUSES with ErrManualTitle when the current TitleSource is
	// TitleSourceManual (manual wins, structurally). `title` is expected to be
	// already clamped by the caller to the naming policy bound; the registry
	// re-clamps defensively to MaxSessionTitleLen runes and single-line, and
	// rejects an empty-after-trim title with ErrInvalidTitle (the auto path
	// never intentionally clears). On success it stores TitleSource =
	// TitleSourceAuto and publishes a content-free session.title_changed
	// (source=auto). `ident` is the run's verified triple; a stored-identity
	// mismatch returns ErrIdentityMismatch, an unknown / erased id returns
	// ErrSessionNotFound.
	SetTitleAuto(ctx context.Context, id string, ident identity.Identity, title string) error

	// CloseRegistry cancels the sweeper goroutine and joins it.
	// Idempotent. Distinct method name (rather than Close) so it
	// doesn't collide with Close(id, reason).
	CloseRegistry(ctx context.Context) error
}

SessionRegistry is the public surface every consumer ( envelope writer, Protocol surface, Console subscribers, etc.) talks to. One concrete impl ships in a later phase (`*Registry`); driver pluralism lives at the StateStore layer.

type SessionReopenedPayload added in v1.14.0

type SessionReopenedPayload struct {
	events.SafeSealed
	// SessionID is the session that was reopened.
	SessionID string
	// ReopenedAt is the reopen timestamp (unix nanoseconds).
	ReopenedAt int64
	// PriorClosedReason is the reason the session had been closed with
	// before this reopen — a GC reason ("gc:idle" / "gc:hard_cap") or the
	// operator-supplied Close reason. A bounded, closed-set-or-operator
	// string, never user content (the Close reason is caller-controlled but
	// MUST NOT carry secret-shaped material — the same contract
	// SessionClosedPayload.Reason follows).
	PriorClosedReason string
}

SessionReopenedPayload reports a session being re-activated from a Closed state (RFC §6.9 amended). Carries the SessionID, the ReopenedAt timestamp, and the PriorClosedReason (the reason the session had been closed with — "gc:idle" / "gc:hard_cap" for a GC reap, or the operator-supplied Close reason). Content-free by construction: NO title, NO user content, NO conversation body — the identity triple lives on the Event itself. Consumers refetch the projection / windowed history for the resumed conversation. SafePayload by construction.

type SessionSnapshot

type SessionSnapshot struct {
	Session
	Running bool
}

SessionSnapshot is the read-side projection returned by Inspect. Carries the lifecycle fields plus a Running boolean derived from the GCPolicy.RunningProbe at inspection time. Running is intrinsically stale by the time the caller reads it; the same is true of any snapshot model.

type SessionTitleChangedPayload added in v1.12.0

type SessionTitleChangedPayload struct {
	events.SafeSealed
	// SessionID is the session whose title changed.
	SessionID string
	// Source is the resulting TitleSource after the call — a three-value
	// domain: "manual" when a caller set a non-empty title via
	// sessions.set_title, "auto" when the internal auto-namer set it via
	// SetTitleAuto, and "" (TitleSourceUnset) when the title was cleared. The
	// wire verb produces only "manual" / ""; the "auto" value comes from the
	// runtime's own auto-naming path.
	Source string
}

SessionTitleChangedPayload reports a successful title change from EITHER producer — the `sessions.set_title` verb (SetTitle: manual set or clear) or the internal auto-namer (SetTitleAuto: auto set). Carries the SessionID and the resulting Source ONLY — the title string itself NEVER rides this payload (the same content-free contract SessionErasedPayload follows): the title is user-derived free text, and SafePayload types are published WITHOUT a redactor pass (the bus skips the redactor for types sealed via SafeSealed), so any raw content here would leak straight to every subscriber. Consumers that want the title text refetch `sessions.list` / `sessions.inspect`.

SafePayload by construction — every field is a bounded id or a closed-set enum string.

type SessionTouchedPayload

type SessionTouchedPayload struct {
	events.SafeSealed
	SessionID string
	LastSeen  int64 // unix nanoseconds
}

SessionTouchedPayload reports a Touch. Carries the SessionID and the new LastSeen timestamp. SafePayload by construction.

type TitleSource added in v1.12.0

type TitleSource string

TitleSource marks who produced a session's Title. The wire verb `sessions.set_title` ALWAYS writes TitleSourceManual (empty title clears both fields to TitleSourceUnset) — TitleSourceAuto is not expressible over the wire; it is declared here for the runtime's internal auto-naming writer, which is the only producer. This makes "manual wins" structurally unforgeable: a caller can never claim `auto` provenance for a title they set themselves.

const (
	// TitleSourceUnset — no title has ever been set, or the title was
	// cleared (an empty SetTitle call resets both Title and
	// TitleSource to this zero value).
	TitleSourceUnset TitleSource = ""
	// TitleSourceAuto — the title was produced by the runtime's internal
	// auto-naming writer. Not reachable via the `sessions.set_title`
	// wire verb.
	TitleSourceAuto TitleSource = "auto"
	// TitleSourceManual — the title was set by an authenticated caller
	// via `sessions.set_title`. Manual titles are never silently
	// overwritten by an auto-namer.
	TitleSourceManual TitleSource = "manual"
)

Directories

Path Synopsis
Package protocol implements the `sessions.*` Protocol methods the Console Sessions page consumes:
Package protocol implements the `sessions.*` Protocol methods the Console Sessions page consumes:

Jump to

Keyboard shortcuts

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