Module: Agent::Sessions

Defined in:
lib/agent/sessions.rb,
lib/agent/sessions/cli.rb,
lib/agent/sessions/node.rb,
lib/agent/sessions/part.rb,
lib/agent/sessions/audit.rb,
lib/agent/sessions/check.rb,
lib/agent/sessions/error.rb,
lib/agent/sessions/store.rb,
lib/agent/sessions/usage.rb,
lib/agent/sessions/sqlite.rb,
lib/agent/sessions/message.rb,
lib/agent/sessions/session.rb,
lib/agent/sessions/version.rb,
lib/agent/sessions/location.rb,
lib/agent/sessions/compaction.rb,
lib/agent/sessions/readers/pi.rb,
lib/agent/sessions/adapters/pi.rb,
lib/agent/sessions/readers/amp.rb,
lib/agent/sessions/adapters/amp.rb,
lib/agent/sessions/env_override.rb,
lib/agent/sessions/readers/base.rb,
lib/agent/sessions/readers/grok.rb,
lib/agent/sessions/readers/qwen.rb,
lib/agent/sessions/adapters/base.rb,
lib/agent/sessions/adapters/grok.rb,
lib/agent/sessions/adapters/qwen.rb,
lib/agent/sessions/readers/codex.rb,
lib/agent/sessions/unknown_agent.rb,
lib/agent/sessions/adapters/codex.rb,
lib/agent/sessions/home_expansion.rb,
lib/agent/sessions/readers/claude.rb,
lib/agent/sessions/readers/gemini.rb,
lib/agent/sessions/adapters/claude.rb,
lib/agent/sessions/adapters/cursor.rb,
lib/agent/sessions/adapters/gemini.rb,
lib/agent/sessions/readers/copilot.rb,
lib/agent/sessions/adapters/copilot.rb,
lib/agent/sessions/readers/opencode.rb,
lib/agent/sessions/unreadable_store.rb,
lib/agent/sessions/adapters/opencode.rb,
lib/agent/sessions/missing_dependency.rb,
lib/agent/sessions/unsupported_format.rb,
lib/agent/sessions/adapters/cursor_ide.rb,
lib/agent/sessions/adapters/enumeration.rb

Defined Under Namespace

Modules: Adapters, HomeExpansion, Readers, Sqlite Classes: Audit, CLI, Check, Compaction, EnvOverride, Error, Location, Message, MissingDependency, Node, Part, Session, Store, UnknownAgent, UnreadableStore, UnsupportedFormat, Usage

Constant Summary collapse

STALE_AFTER_DAYS =
90
VERIFIED_ON =

The oldest verified_on among the built-in adapters. A claim about somebody else's software is only as current as its weakest link, so this is the honest answer to "when was this last known to be true".

registry.values.map(&:verified_on_date).min
VERSION =
"0.3.1"

Class Method Summary collapse

Class Method Details

.agentsObject



34
# File 'lib/agent/sessions.rb', line 34

def agents = registry.keys

.all(env: ENV) ⇒ Object



40
41
42
# File 'lib/agent/sessions.rb', line 40

def all(env: ENV)
  registry.keys.map { |agent| locate(agent, env: env) }
end

.audit(env: ENV) ⇒ Object



169
170
171
# File 'lib/agent/sessions.rb', line 169

def audit(env: ENV)
  Audit.new(all(env: env), env: env).report
end

.doctor(agent = nil, env: ENV, today: Date.today) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/agent/sessions.rb', line 154

def doctor(agent = nil, env: ENV, today: Date.today)
  targets = agent ? [adapter_for(agent)] : registry.values
  staleness = targets.map do |klass|
    age = (today - klass.verified_on_date).to_i
    if age > STALE_AFTER_DAYS
      Check.new(agent: klass.agent_name, status: :drift, claim: "verified within #{STALE_AFTER_DAYS} days",
                detail: "last verified #{klass.verified_on_date} (#{age} days ago)")
    else
      Check.new(agent: klass.agent_name, status: :pass, claim: "verified within #{STALE_AFTER_DAYS} days",
                detail: "last verified #{klass.verified_on_date}")
    end
  end
  verify(agent, env: env) + staleness
end

.for_project(dir, env: ENV, agents: nil) ⇒ Object

One project across every agent (or the agents: subset), lazily: adapters earlier in the sweep satisfy first(n) without the later ones ever being asked. Within one adapter, though, laziness cannot skip non-matching sessions — sessions_for_project must still stat and check each candidate to know it does not match, so an adapter with zero matches costs a full scan of its store before the sweep moves on. True of the six Base-driven adapters; opencode pushes the filter into SQL (WHERE directory = ?) and stats nothing.

Deliberately does NOT rescue MissingDependency or UnreadableStore: opencode without the sqlite3 gem, or with a corrupt/locked database, raises. Since flat_map is lazy, that raise surfaces only once enumeration reaches the failing adapter — possibly after other agents' sessions have already been yielded to the caller mid-iteration, and possibly not at all if first(n) is satisfied first. Silently omitting an agent's sessions is this gem's worst failure mode (design doc decision 11), so this method never trades a raised, attributable error for a quietly incomplete list. A caller that wants the sweep to survive one bad agent should rescue per call, e.g. by driving agents: itself and catching around each adapter; a caller who just wants to route around a known-bad agent can pass agents: naming every registered agent except it. The CLI (Task 10) does the former, turning the same exceptions into per-agent "skipped" lines instead of one failed sweep. Note a rescue cannot resume this enumerator: re-calling each after a raise re-raises from the same adapter. The only recovery is a fresh call with a narrower agents:. Adapters are resolved eagerly, so an unknown name in agents: raises here rather than mid-sweep. The deferral above is about DATA conditions, where the raise carries information about a store; a typo'd agent symbol is a programmer error knowable before any I/O, and agents: [:claude, :nope] otherwise hands back Claude's sessions and then crashes. Base#initialize only stores @env, so constructing all seven up front costs nothing, and the sweep stays lazy — first(n) still stops at the first matching adapter.



95
96
97
98
99
# File 'lib/agent/sessions.rb', line 95

def for_project(dir, env: ENV, agents: nil)
  dir = File.expand_path(dir)
  adapters = (agents || registry.keys).map { |name| adapter_for(name).new(env: env) }
  adapters.lazy.flat_map { |adapter| adapter.sessions_for_project(dir) }
end

.installed(env: ENV) ⇒ Object



44
45
46
# File 'lib/agent/sessions.rb', line 44

def installed(env: ENV)
  all(env: env).select(&:installed?)
end

.locate(agent, env: ENV) ⇒ Object



36
37
38
# File 'lib/agent/sessions.rb', line 36

def locate(agent, env: ENV)
  adapter_for(agent).new(env: env).locate
end

.projects(agent, env: ENV) ⇒ Object

Eager, unlike sessions/for_project: project_paths already reads every session to answer (design doc section 7 — the on-disk encodings are lossy, so the recorded cwd is the only reliable source), sorts, and dedupes, so a lazy return type here would promise a laziness the work underneath cannot honor. Returns a plain, already-sorted Array. Raises MissingDependency or UnreadableStore for opencode, as sessions does.



107
108
109
# File 'lib/agent/sessions.rb', line 107

def projects(agent, env: ENV)
  adapter_for(agent).new(env: env).project_paths
end

.read(session, **options) ⇒ Object

Layer 3. Takes a Session (from sessions, for_project), not an agent name, because reading is per-session — the adapter comes from the session itself. Raises UnsupportedFormat for an agent with no reader yet, rather than returning a reader that yields nothing: "this gem cannot read that format" and "that session has no messages" must never look alike.

include_events: true adds the agent's UI-level records to the stream where an adapter has them. They are excluded by default because they are bookkeeping, not conversation, and they outnumber real messages.



139
140
141
142
143
144
145
146
147
# File 'lib/agent/sessions.rb', line 139

def read(session, **options)
  klass = adapter_for(session.agent).reader_class
  unless klass
    raise UnsupportedFormat,
          "no reader for #{session.agent} yet; Session#fidelity says #{session.fidelity}"
  end

  klass.new(session, **options)
end

.register(adapter_class) ⇒ Object

Re-registering a name deliberately replaces it, so a consumer can ship a corrected adapter for an agent whose layout moved before the gem catches up.

Raises:



23
24
25
26
27
28
# File 'lib/agent/sessions.rb', line 23

def register(adapter_class)
  name = adapter_class.agent_name
  raise Error, "#{adapter_class.inspect} declares no agent name" if name.nil?

  registry[name] = adapter_class
end

.registryObject



30
31
32
# File 'lib/agent/sessions.rb', line 30

def registry
  @registry ||= {}
end

.sessions(agent, env: ENV, since: nil) ⇒ Object

Lazy: consuming N sessions stats N files, never more. since, when given, must be a Time (or anything Time#>= accepts) — comparing updated_at (always a Time; every adapter populates it, from mtime or store metadata) against a Date, Integer, or String raises ArgumentError("comparison of Time with ... failed"), which already names the mistake, so no extra guard is added here. That raise happens on enumeration, not on this call, because the filter itself is lazy — sessions(:x, since: bad).first(1) can raise from inside first, not from this line. Raises MissingDependency or UnreadableStore for opencode under the same conditions described on for_project below.



58
59
60
61
# File 'lib/agent/sessions.rb', line 58

def sessions(agent, env: ENV, since: nil)
  list = adapter_for(agent).new(env: env).sessions
  since ? list.select { |session| session.updated_at >= since } : list
end

.unresolved_project_count(agent, env: ENV) ⇒ Object

Companion to projects/project_paths, which both exclude a session whose project could not be resolved rather than counting it (design doc section 7) — so "this agent genuinely records no projects" and "this agent's project resolution is broken" read identically from the outside. Three of seven adapters can legitimately return a nil project_path (Amp threads with no trees, cursor_ide by design, pi whenever its unverified header assumption is wrong); this counts it for any of them, uniformly, using only the public sessions enumerator — no adapter needs to know this exists. Eager and a second full sweep of the store, same cost class as projects itself, so it is opt-in (called by the CLI only under list --project, never under plain list) rather than folded into projects' own return value, which is a documented, tested plain Array and would otherwise need a shape change to carry both numbers. Raises MissingDependency or UnreadableStore for opencode, as sessions does.



126
127
128
# File 'lib/agent/sessions.rb', line 126

def unresolved_project_count(agent, env: ENV)
  adapter_for(agent).new(env: env).sessions.count { |session| session.project_path.nil? }
end

.verify(agent = nil, env: ENV) ⇒ Object



149
150
151
152
# File 'lib/agent/sessions.rb', line 149

def verify(agent = nil, env: ENV)
  targets = agent ? [adapter_for(agent)] : registry.values
  targets.flat_map { |klass| klass.new(env: env).verify }
end