Class: Agent::Sessions::Adapters::Grok

Inherits:
Base
  • Object
show all
Defined in:
lib/agent/sessions/adapters/grok.rb

Overview

Grok Build (xAI). PROVISIONAL: ~/.grok does not exist on the machine this was written on (2026-08-24), so every claim follows tokentelemetry's working parser of the same store (resources/tokentelemetry, _scan_grok_sessions and _grok_usage_from_unified_log) rather than observation.

A Grok session is a DIRECTORY, not a file:

~/.grok/sessions/<url-encoded cwd>/<session-uuid>/
summary.json  chat_history.jsonl  events.jsonl  updates.jsonl
signals.json  plan_mode.json  subagents/<spawn-id>/meta.json

summary.json is what this adapter enumerates, because it is the record that always exists and carries the session's own metadata. The transcript beside it is what the reader reads, and bytes counts the whole directory — the same choice Claude's adapter makes for its sidecar tree, and for the same reason: those bytes belong to this session, and a du that ignored them would disagree with the disk.

Constant Summary

Constants inherited from Base

Base::FIDELITIES

Constants included from Enumeration

Enumeration::MAX_LINE_BYTES

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#base_dir, fidelity_value, homedir_config, #initialize, #locate, #retention, #retention_source, store_configs, #verify

Methods included from Enumeration

#project_paths, #sessions, #sessions_for_project

Constructor Details

This class inherits a constructor from Agent::Sessions::Adapters::Base

Class Method Details

.reader_classObject



30
# File 'lib/agent/sessions/adapters/grok.rb', line 30

def self.reader_class = Readers::Grok

Instance Method Details

#bytes_for(path, stat) ⇒ Object

The whole session directory, not just summary.json: the transcript and every sibling log live in it.



101
102
103
104
105
106
107
108
# File 'lib/agent/sessions/adapters/grok.rb', line 101

def bytes_for(path, stat)
  dir = File.dirname(path)
  Dir.glob(File.join(escape_glob(dir), "**", "*"), File::FNM_DOTMATCH).sum do |entry|
    File.file?(entry) ? File.size(entry) : 0
  rescue SystemCallError
    0
  end
end

#decode_project(name) ⇒ Object



81
82
83
84
85
# File 'lib/agent/sessions/adapters/grok.rb', line 81

def decode_project(name)
  URI.decode_www_form_component(name)
rescue ArgumentError # a name that is not valid percent-encoding
  name
end

#encode_project(dir) ⇒ Object



74
75
76
77
78
79
# File 'lib/agent/sessions/adapters/grok.rb', line 74

def encode_project(dir)
  # Percent-encode everything a path separator is not, matching what
  # URL-encoding a whole path produces. CGI.escape is deliberately not
  # used: it encodes a space as "+", which decodes back to "+" here.
  URI.encode_www_form_component(dir).gsub("+", "%20")
end

#project_dir_name(path) ⇒ Object



70
71
72
# File 'lib/agent/sessions/adapters/grok.rb', line 70

def project_dir_name(path)
  File.basename(File.dirname(File.dirname(path)))
end

#project_path_for(path) ⇒ Object

The project bucket is a URL-encoded absolute path (tokentelemetry unquotes it), so unlike Claude's and pi's dash encodings this one is losslessly reversible. summary.json's own info.cwd is preferred where readable, because a recorded path beats a decoded directory name; the decode is the fallback, and a good one.



63
64
65
66
67
68
# File 'lib/agent/sessions/adapters/grok.rb', line 63

def project_path_for(path)
  recorded = read_json(path).dig("info", "cwd")
  return recorded if recorded.is_a?(String)

  decode_project(File.basename(File.dirname(File.dirname(path))))
end

#session_id_from(path) ⇒ Object

///summary.json — the id is the directory holding the file, not the file's own basename, which is the constant "summary".



54
55
56
# File 'lib/agent/sessions/adapters/grok.rb', line 54

def session_id_from(path)
  File.basename(File.dirname(path))
end

#started_at_for(path, stat) ⇒ Object

summary.json carries the session's own clock; the file's mtime is only ever a proxy for it. Both are ISO 8601 strings per the reference parser, with created_at standing in when updated_at is absent.



90
91
92
# File 'lib/agent/sessions/adapters/grok.rb', line 90

def started_at_for(path, stat)
  parse_time(read_json(path)["created_at"]) || super
end

#updated_at_for(path, stat) ⇒ Object



94
95
96
97
# File 'lib/agent/sessions/adapters/grok.rb', line 94

def updated_at_for(path, stat)
  summary = read_json(path)
  parse_time(summary["updated_at"]) || parse_time(summary["created_at"]) || super
end

#warningsObject



41
42
43
44
45
46
47
48
49
# File 'lib/agent/sessions/adapters/grok.rb', line 41

def warnings
  list = super
  if primary_layer.exists?
    list << "Grok's store shape is unverified — no ~/.grok existed on the machine this " \
            "adapter was written on, so it follows tokentelemetry's parser of the same " \
            "format. Please open an issue if sessions, projects or usage look wrong."
  end
  list
end