Class: Agent::Sessions::Adapters::Opencode
- Defined in:
- lib/agent/sessions/adapters/opencode.rb
Constant Summary collapse
- DATABASE_GLOB =
opencode names its database per release channel — opencode.db, opencode-stable.db — so the filename is a glob, not a constant (tokentelemetry globs the same pattern). Unverified here: this machine has only the plain name.
"opencode*.db"- SESSION_COLUMNS =
"id, directory, time_created, time_updated"
Constants inherited from Base
Constants included from Enumeration
Class Method Summary collapse
Instance Method Summary collapse
-
#base_dir ⇒ Object
The declared default stands unless another candidate actually holds a database.
-
#project_paths ⇒ Object
ORDER BY matches the sorted order Base guarantees, so
projectsoutput is stable and diffable whichever adapter answers it. -
#sessions ⇒ Object
Sessions are rows, not files, so the Base glob enumeration is replaced by a deferred query: it runs at first consumption, and only if the database exists.
-
#sessions_for_project(dir) ⇒ Object
The directory column holds the full recorded path, so filtering is a WHERE clause instead of the Base read-and-compare loop.
-
#verify ⇒ Object
Base checks the declared path literally, which gets a machine holding only a channel-named database wrong twice: its "is this agent installed" gate sees no declared layer and skips the store checks entirely, and the store check itself would report :fail on a real store that is merely called something the declaration did not predict.
Methods inherited from Base
fidelity_value, homedir_config, #initialize, #locate, #retention, #retention_source, store_configs, #warnings
Methods included from Enumeration
#bytes_for, #encode_project, #project_dir_name, #project_path_for, #session_id_from, #started_at_for, #updated_at_for
Constructor Details
This class inherits a constructor from Agent::Sessions::Adapters::Base
Class Method Details
Instance Method Details
#base_dir ⇒ Object
The declared default stands unless another candidate actually holds a
database. Falling back to it rather than to the first candidate that
merely exists keeps where printing a concrete, conventional path on
a machine with no opencode at all.
27 28 29 30 31 32 |
# File 'lib/agent/sessions/adapters/opencode.rb', line 27 def base_dir @base_dir ||= begin probes = ([resolver.home(:opencode)] + resolver.candidates(:opencode)).map(&:to_s).uniq probes.find { |dir| Dir.glob(File.join(escape_glob(dir), DATABASE_GLOB)).any? } || resolver.home(:opencode).to_s end end |
#project_paths ⇒ Object
ORDER BY matches the sorted order Base guarantees, so projects output is
stable and diffable whichever adapter answers it. is_a?(String) excludes
a row whose directory is NULL (build_db_session's guard, same rule 2
container check) rather than letting a literal nil sort in among real
paths — "excluded, not nil", matching Base's project_paths docstring.
.uniq is needed on top of SQL's own DISTINCT: SQLite's DISTINCT treats a
BLOB and a byte-identical TEXT value as different rows (confirmed
directly — typeof reports "text" vs "blob" for the same bytes even
though the sqlite3 gem returns both to Ruby as String, see
build_db_session's comment), so without this a blob/text pair with
identical bytes would surface as two entries where Base's own
.uniq.sort would collapse them to one.
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/agent/sessions/adapters/opencode.rb', line 134 def project_paths paths = [] database_paths.each do |db_path| each_session_row(db_path, "SELECT DISTINCT directory FROM session ORDER BY directory") do |row| paths << row.first if row.first.is_a?(String) end end # Sorted after the union, not per database: SQL's ORDER BY only orders # within one file, and two channel databases concatenated would leave # `projects` unsorted — the one thing Base guarantees about it. paths.uniq.sort end |
#sessions ⇒ Object
Sessions are rows, not files, so the Base glob enumeration is replaced by a deferred query: it runs at first consumption, and only if the database exists. The existence check comes FIRST so machines without opencode never need sqlite3 at all (design doc section 9).
Row order is deliberately unspecified: no ORDER BY, so rows arrive in whatever order SQLite's own scan produces (rowid order, absent an index that would change it) — unlike the other six adapters, which are path-sorted for free by Dir.glob. Invisible today because nothing here sorts before Task 10 does its own sort_by(&:updated_at); stated so a future caller of THIS method directly does not come to depend on insertion order looking stable.
The gap between this check and the open below is a real TOCTOU window — opencode could delete or migrate the file in between — but the open that follows a vanished file raises SQLite3::CantOpenException (verified directly), which each_session_row already turns into UnreadableStore. That is judged the right answer, not a bug to special-case: unlike Base's own glob-then-stat race (one file silently missing from a multi-file listing, so build_session's rescue drops it and moves on), a vanished DATABASE is the store's only source for every session, so there is nothing partial to return — "the store I just confirmed exists is now unreadable" is what happened, and UnreadableStore says exactly that.
Opens once per consumption, not once per instance: sessions,
sessions_for_project and project_paths each open, query and close
their own connection through each_session_row. That costs an extra
open when a caller uses more than one of the three, but keeps every
method independently correct rather than threading a shared handle
through them — and Enumerator.new's block does not even run until the
RETURNED lazy enumerator is consumed, so a caller that builds sessions
and never touches it opens nothing at all. Confirmed empirically (not
just assumed from Enumerator's docs) that the ensure db&.close inside
each_session_row fires promptly either way a caller can stop early —
.lazy.first(n) and an external each { break } both unwind the
generator fiber immediately, before the outer call returns — so a
caller taking sessions.first never leaves a connection open waiting
for GC to reclaim the fiber.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/agent/sessions/adapters/opencode.rb', line 81 def sessions paths = database_paths return [].lazy if paths.empty? Enumerator.new do |yielder| seen = {} paths.each do |db_path| each_session_row(db_path, "SELECT #{SESSION_COLUMNS} FROM session") do |row| # Channel databases can hold the same session — a store migrated # between channels keeps both files. Deduped by id, first # database wins, so one session is one row to a caller counting # them. tokentelemetry dedups the same way for the same reason. next if seen[row.first] seen[row.first] = true yielder << build_db_session(db_path, row) end end end.lazy end |
#sessions_for_project(dir) ⇒ Object
The directory column holds the full recorded path, so filtering is a WHERE clause instead of the Base read-and-compare loop.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/agent/sessions/adapters/opencode.rb', line 104 def sessions_for_project(dir) dir = File.(dir) paths = database_paths return [].lazy if paths.empty? Enumerator.new do |yielder| seen = {} paths.each do |db_path| each_session_row(db_path, "SELECT #{SESSION_COLUMNS} FROM session WHERE directory = ?", [dir]) do |row| next if seen[row.first] seen[row.first] = true yielder << build_db_session(db_path, row) end end end.lazy end |
#verify ⇒ Object
Base checks the declared path literally, which gets a machine holding only a channel-named database wrong twice: its "is this agent installed" gate sees no declared layer and skips the store checks entirely, and the store check itself would report :fail on a real store that is merely called something the declaration did not predict.
Any file matching the glob satisfies the claim. The detail names what was actually found, so a non-canonical filename is visible rather than merely tolerated — the same reason detail_for prints a file count.
156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/agent/sessions/adapters/opencode.rb', line 156 def verify found = database_paths return super if found.empty? checks = super database = checks.find { |candidate| candidate.claim == "store database exists" } return checks.map { |c| c.claim == database&.claim && !c.pass? ? passing_database(found) : c } if database # The skip gate fired: Base returned one :skip and never looked at the # stores. Answer the store claim it never asked. [passing_database(found)] + checks.reject { |c| c.claim == "agent is installed" } end |