Module: Harnex::DispatchHistory
- Defined in:
- lib/harnex/dispatch_history.rb
Constant Summary collapse
- MAX_REPO_WALK_LEVELS =
10- SCHEMA_VERSION =
v2 marks the unified era: one rich dispatch_end row per dispatch carrying both the thin envelope and the summary sections. Readers key on record_type, not this stamp; legacy clauses keep accepting v1 and envelope-less rows mixed in the same file.
2
Class Method Summary collapse
- .append(path, record) ⇒ Object
-
.build_record(session) ⇒ Object
The v2 end row: the thin envelope merged with the rich summary sections.
-
.build_start_record(session) ⇒ Object
Appended at registration so a running dispatch always has a durable trace; the dispatch_end row written in finalize_session! completes it.
- .classify(session) ⇒ Object
- .commit_sha(git_start, git_end) ⇒ Object
- .end_matches_start?(end_record, start_record) ⇒ Boolean
- .end_record?(record) ⇒ Boolean
- .find_git_root(start_path) ⇒ Object
- .git_toplevel(path) ⇒ Object
- .global_path ⇒ Object
-
.latest_rows(path, id) ⇒ Object
Latest start row for
idand any end row that completes it. -
.live_start_record(repo_root:, id:) ⇒ Object
A dispatch_start row with no completing end row whose pid is alive on this host — evidence of a running session even when the live registry is not visible from the caller's context.
- .path_for(start_path = Dir.pwd, global: false) ⇒ Object
- .same_host?(record) ⇒ Boolean
- .start_record?(record) ⇒ Boolean
- .tmux_state(tmux_session) ⇒ Object
Class Method Details
.append(path, record) ⇒ Object
60 61 62 63 64 65 66 67 68 69 |
# File 'lib/harnex/dispatch_history.rb', line 60 def append(path, record) FileUtils.mkdir_p(File.dirname(path)) line = JSON.generate(record) + "\n" File.open(path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |file| file.flock(File::LOCK_EX) file.write(line) ensure file.flock(File::LOCK_UN) unless file.closed? end end |
.build_record(session) ⇒ Object
The v2 end row: the thin envelope merged with the rich summary sections. The envelope carries no raw meta passthrough — the summary's meta section (a superset with provenance) rides in its place; top-level tier stays for the history renderer.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/harnex/dispatch_history.rb', line 168 def build_record(session) ended_at = session.ended_at || Time.now status, terminal_event = classify(session) { schema_version: SCHEMA_VERSION, record_type: "dispatch_end", id: session.id, session_id: session.session_id, description: session.description, cli: session.adapter.key, started_at: session.started_at.utc.iso8601, ended_at: ended_at.utc.iso8601, duration_s: (ended_at - session.started_at).to_i, status: status, terminal_event: terminal_event, commit_sha: commit_sha(session.git_start, session.git_end), tier: session.__send__(:meta_hash)["tier"], summary_out_path: session.summary_out, events_log_path: session.events_log_path, tmux_state: tmux_state(session.__send__(:summary_tmux_session)) }.merge(session.__send__(:build_summary_record)) end |
.build_start_record(session) ⇒ Object
Appended at registration so a running dispatch always has a durable trace; the dispatch_end row written in finalize_session! completes it.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/harnex/dispatch_history.rb', line 145 def build_start_record(session) { schema_version: SCHEMA_VERSION, record_type: "dispatch_start", id: session.id, session_id: session.session_id, pid: session.pid, host: Harnex.host_info[:host], cli: session.adapter.key, description: session.description, started_at: session.started_at.utc.iso8601, repo_root: session.repo_root, tier: session.__send__(:meta_hash)["tier"], meta: session.__send__(:meta_hash), summary_out_path: session.summary_out, events_log_path: session.events_log_path } end |
.classify(session) ⇒ Object
191 192 193 194 195 196 197 198 199 |
# File 'lib/harnex/dispatch_history.rb', line 191 def classify(session) return ["failed", "task_failed"] if session.respond_to?(:task_failed?) && session.task_failed? return ["completed", "task_complete"] if session.task_complete? return ["timeout", "timeout"] if session.exit_code == 124 return ["killed", "process_kill"] if session.term_signal return ["completed", "process_exit"] if session.exit_code == 0 ["failed", "dispatch_failed"] end |
.commit_sha(git_start, git_end) ⇒ Object
201 202 203 204 205 206 207 |
# File 'lib/harnex/dispatch_history.rb', line 201 def commit_sha(git_start, git_end) start_sha = git_start[:sha].to_s end_sha = git_end[:sha].to_s return nil if start_sha.empty? || end_sha.empty? || start_sha == end_sha end_sha end |
.end_matches_start?(end_record, start_record) ⇒ Boolean
83 84 85 86 87 88 89 90 |
# File 'lib/harnex/dispatch_history.rb', line 83 def end_matches_start?(end_record, start_record) end_session = end_record["session_id"].to_s start_session = start_record["session_id"].to_s return end_session == start_session unless end_session.empty? || start_session.empty? end_record["id"].to_s == start_record["id"].to_s && end_record["started_at"].to_s == start_record["started_at"].to_s end |
.end_record?(record) ⇒ Boolean
75 76 77 78 79 80 81 |
# File 'lib/harnex/dispatch_history.rb', line 75 def end_record?(record) return false unless record.is_a?(Hash) return true if record["record_type"] == "dispatch_end" # Legacy end rows predate record_type. record["schema_version"] == 1 && record.key?("status") && !record.key?("record_type") end |
.find_git_root(start_path) ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/harnex/dispatch_history.rb', line 31 def find_git_root(start_path) path = File.(start_path.to_s.empty? ? Dir.pwd : start_path) path = File.dirname(path) unless File.directory?(path) git_root = git_toplevel(path) return git_root if git_root (MAX_REPO_WALK_LEVELS + 1).times do return path if File.directory?(File.join(path, ".git")) parent = File.dirname(path) break if parent == path path = parent end nil end |
.git_toplevel(path) ⇒ Object
50 51 52 53 54 55 56 57 58 |
# File 'lib/harnex/dispatch_history.rb', line 50 def git_toplevel(path) output, status = Open3.capture2("git", "-C", path, "rev-parse", "--show-toplevel", err: File::NULL) root = output.to_s.strip return root if status.success? && !root.empty? nil rescue StandardError nil end |
.global_path ⇒ Object
18 19 20 |
# File 'lib/harnex/dispatch_history.rb', line 18 def global_path File.join(STATE_DIR, "dispatch.jsonl") end |
.latest_rows(path, id) ⇒ Object
Latest start row for id and any end row that completes it.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/harnex/dispatch_history.rb', line 93 def latest_rows(path, id) latest_start = nil matching_end = nil return { start: nil, end: nil } unless File.file?(path) File.foreach(path) do |line| record = JSON.parse(line) next unless record.is_a?(Hash) next unless record["id"].to_s == id if start_record?(record) latest_start = record matching_end = nil elsif end_record?(record) matching_end = record if latest_start && end_matches_start?(record, latest_start) end rescue JSON::ParserError next end { start: latest_start, end: matching_end } end |
.live_start_record(repo_root:, id:) ⇒ Object
A dispatch_start row with no completing end row whose pid is alive on this host — evidence of a running session even when the live registry is not visible from the caller's context.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/harnex/dispatch_history.rb', line 120 def live_start_record(repo_root:, id:) normalized_id = Harnex.normalize_id(id) rows = latest_rows(path_for(repo_root), normalized_id) start = rows[:start] return nil unless start return nil if rows[:end] return nil unless same_host?(start) pid = start["pid"] return nil unless pid && Harnex.alive_pid?(pid) start rescue StandardError nil end |
.path_for(start_path = Dir.pwd, global: false) ⇒ Object
22 23 24 25 26 27 28 29 |
# File 'lib/harnex/dispatch_history.rb', line 22 def path_for(start_path = Dir.pwd, global: false) return global_path if global repo_root = find_git_root(start_path) return global_path unless repo_root File.join(repo_root, ".harnex", "dispatch.jsonl") end |
.same_host?(record) ⇒ Boolean
136 137 138 139 140 141 |
# File 'lib/harnex/dispatch_history.rb', line 136 def same_host?(record) host = record["host"].to_s return true if host.empty? host == Harnex.host_info[:host].to_s end |
.start_record?(record) ⇒ Boolean
71 72 73 |
# File 'lib/harnex/dispatch_history.rb', line 71 def start_record?(record) record.is_a?(Hash) && record["record_type"] == "dispatch_start" end |
.tmux_state(tmux_session) ⇒ Object
209 210 211 212 213 214 215 |
# File 'lib/harnex/dispatch_history.rb', line 209 def tmux_state(tmux_session) return "torn-down" if tmux_session.to_s.empty? system("tmux", "has-session", "-t", tmux_session.to_s, out: File::NULL, err: File::NULL) ? "live" : "torn-down" rescue StandardError "torn-down" end |