carson 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/RELEASE.md +20 -0
- data/VERSION +1 -1
- data/lib/carson/runtime/session.rb +77 -10
- data/lib/carson/runtime/status.rb +53 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffbada076dd17e9f8ae9a85e9abce9abbc84e23b71324c8cde1ffec1db537071
|
|
4
|
+
data.tar.gz: 4654c5d248f856818ffa83600b4db21838c0f665bb6a16f2d45d566643312ec6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4e517ccc14a75a2389bf5743fdd095b2796605c5c7fbeb4d12af4b0f237f370a275e2ff244436da4439b10e44b38a7aceae44c26b858d2224a8b0a25b1d193f
|
|
7
|
+
data.tar.gz: a1de0764868ee6fda99d4f74f5ec10dfc2071a84e65102ab77749e83a8307aee37aa661d98b3670e1d62aeda28f1be1a05093283ac697de3a39ad542256e6da9
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,26 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
+
## 3.9.0 — Agent Coordination Signals
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Per-session state files** — each Runtime instance gets a unique session ID (`<pid>-<timestamp>`) and its own session file at `~/.carson/sessions/<repo_slug>/<session_id>.json`. Multiple agents on the same repo each have independent state that does not collide.
|
|
13
|
+
- **Session ownership on worktrees** — `carson status` cross-references active session files to annotate each worktree with its owning session's PID, task, and staleness. Other agents can see which worktrees are in use and by whom.
|
|
14
|
+
- **Staleness detection** — sessions whose PID is no longer running and whose last update is older than 1 hour are marked as `stale`. This signals to other agents that the worktree may be abandoned and available for cleanup.
|
|
15
|
+
- **`session_list`** — new method that scans all session files for the current repo and returns structured data with staleness annotations.
|
|
16
|
+
- **Migration from 3.8 format** — old single-file session state (`<slug>.json`) is automatically migrated to the per-session directory format on first access.
|
|
17
|
+
|
|
18
|
+
### UX
|
|
19
|
+
|
|
20
|
+
- `carson status` worktree listing now shows owner context: task description if set, PID if no task, or "stale session" if the owner is no longer running.
|
|
21
|
+
- `carson session --json` now includes `session_id` and `pid` fields for correlation by other agents.
|
|
22
|
+
- Human output unchanged when no session ownership data exists.
|
|
23
|
+
|
|
24
|
+
### Migration
|
|
25
|
+
|
|
26
|
+
- Automatic. The old `<slug>.json` file is moved into the new `<slug>/` directory as `migrated.json` on first access. No user action required.
|
|
27
|
+
|
|
8
28
|
## 3.8.0 — Session State
|
|
9
29
|
|
|
10
30
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.9.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Session state persistence for coding agents.
|
|
2
|
-
# Maintains a lightweight JSON file per
|
|
3
|
-
# agents can discover the current working context without re-running discovery commands.
|
|
2
|
+
# Maintains a lightweight JSON file per session in ~/.carson/sessions/<repo_slug>/
|
|
3
|
+
# so agents can discover the current working context without re-running discovery commands.
|
|
4
|
+
# Multiple agents on the same repo each get their own session file.
|
|
4
5
|
# Respects the outsider boundary: state lives in Carson's own space, not the repository.
|
|
5
6
|
require "digest"
|
|
6
7
|
|
|
@@ -25,7 +26,7 @@ module Carson
|
|
|
25
26
|
)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
# Clears session state for
|
|
29
|
+
# Clears session state for the current session.
|
|
29
30
|
def session_clear!( json_output: false )
|
|
30
31
|
path = session_file_path
|
|
31
32
|
File.delete( path ) if File.exist?( path )
|
|
@@ -54,19 +55,55 @@ module Carson
|
|
|
54
55
|
|
|
55
56
|
state[ :task ] = task if task
|
|
56
57
|
state[ :repo ] = repo_root
|
|
58
|
+
state[ :session_id ] = session_id
|
|
59
|
+
state[ :pid ] = Process.pid
|
|
57
60
|
state[ :updated_at ] = Time.now.utc.iso8601
|
|
58
61
|
|
|
59
62
|
write_session( state )
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
# Returns all active sessions for this repository.
|
|
66
|
+
# Each entry is a parsed session state hash with staleness annotation.
|
|
67
|
+
def session_list
|
|
68
|
+
dir = session_repo_dir
|
|
69
|
+
return [] unless Dir.exist?( dir )
|
|
70
|
+
|
|
71
|
+
Dir.glob( File.join( dir, "*.json" ) ).filter_map do |path|
|
|
72
|
+
data = JSON.parse( File.read( path ), symbolize_names: true ) rescue next
|
|
73
|
+
data[ :stale ] = session_stale?( data )
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
62
78
|
private
|
|
63
79
|
|
|
64
|
-
# Returns
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
# Returns a stable session identifier for this Runtime instance.
|
|
81
|
+
# PID + start timestamp — unique per process, stable across calls.
|
|
82
|
+
def session_id
|
|
83
|
+
@session_id ||= "#{Process.pid}-#{Time.now.utc.strftime( '%Y%m%d%H%M%S' )}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the per-repo session directory: ~/.carson/sessions/<slug>/
|
|
87
|
+
# Migrates from the pre-3.9 single-file format if found.
|
|
88
|
+
def session_repo_dir
|
|
68
89
|
slug = session_repo_slug
|
|
69
|
-
File.join(
|
|
90
|
+
dir = File.join( carson_home, "sessions", slug )
|
|
91
|
+
|
|
92
|
+
# Migrate from pre-3.9 single-file format: <slug>.json → <slug>/<session_id>.json
|
|
93
|
+
old_file = File.join( carson_home, "sessions", "#{slug}.json" )
|
|
94
|
+
if File.file?( old_file ) && !Dir.exist?( dir )
|
|
95
|
+
FileUtils.mkdir_p( dir )
|
|
96
|
+
migrated = File.join( dir, "migrated.json" )
|
|
97
|
+
FileUtils.mv( old_file, migrated )
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
FileUtils.mkdir_p( dir )
|
|
101
|
+
dir
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns the session file path for the current session.
|
|
105
|
+
def session_file_path
|
|
106
|
+
File.join( session_repo_dir, "#{session_id}.json" )
|
|
70
107
|
end
|
|
71
108
|
|
|
72
109
|
# Generates a readable, unique slug for the repository: basename-shortsha.
|
|
@@ -87,13 +124,14 @@ module Carson
|
|
|
87
124
|
# Reads session state from disk. Returns an empty hash if no state exists.
|
|
88
125
|
def read_session
|
|
89
126
|
path = session_file_path
|
|
90
|
-
return { repo: repo_root } unless File.exist?( path )
|
|
127
|
+
return { repo: repo_root, session_id: session_id } unless File.exist?( path )
|
|
91
128
|
|
|
92
129
|
data = JSON.parse( File.read( path ), symbolize_names: true )
|
|
93
130
|
data[ :repo ] = repo_root
|
|
131
|
+
data[ :session_id ] = session_id
|
|
94
132
|
data
|
|
95
133
|
rescue JSON::ParserError, StandardError
|
|
96
|
-
{ repo: repo_root }
|
|
134
|
+
{ repo: repo_root, session_id: session_id }
|
|
97
135
|
end
|
|
98
136
|
|
|
99
137
|
# Writes session state to disk as formatted JSON.
|
|
@@ -112,6 +150,35 @@ module Carson
|
|
|
112
150
|
end
|
|
113
151
|
end
|
|
114
152
|
|
|
153
|
+
# Detects whether a session is stale — PID no longer running and updated
|
|
154
|
+
# more than 1 hour ago.
|
|
155
|
+
def session_stale?( data )
|
|
156
|
+
pid = data[ :pid ]
|
|
157
|
+
updated = data[ :updated_at ]
|
|
158
|
+
|
|
159
|
+
# If PID is still running, not stale.
|
|
160
|
+
if pid
|
|
161
|
+
begin
|
|
162
|
+
Process.kill( 0, pid.to_i )
|
|
163
|
+
return false
|
|
164
|
+
rescue Errno::ESRCH
|
|
165
|
+
# Process not running — check age.
|
|
166
|
+
rescue Errno::EPERM
|
|
167
|
+
# Process exists but we lack permission — assume active.
|
|
168
|
+
return false
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# If no timestamp, assume stale.
|
|
173
|
+
return true unless updated
|
|
174
|
+
|
|
175
|
+
# Stale if last updated more than 1 hour ago.
|
|
176
|
+
age = Time.now.utc - Time.parse( updated )
|
|
177
|
+
age > 3600
|
|
178
|
+
rescue StandardError
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
|
|
115
182
|
# Unified output for session results — JSON or human-readable.
|
|
116
183
|
def session_finish( result:, exit_code:, json_output: )
|
|
117
184
|
result[ :exit_code ] = exit_code
|
|
@@ -81,17 +81,47 @@ module Carson
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Lists all worktrees with branch and
|
|
84
|
+
# Lists all worktrees with branch, lifecycle state, and session ownership.
|
|
85
85
|
def gather_worktree_info
|
|
86
86
|
entries = worktree_list
|
|
87
|
+
sessions = session_list
|
|
88
|
+
ownership = build_worktree_ownership( sessions: sessions )
|
|
89
|
+
|
|
87
90
|
# Filter out the main worktree (the repository root itself).
|
|
88
91
|
entries.reject { |wt| wt.fetch( :path ) == repo_root }.map do |wt|
|
|
89
|
-
|
|
92
|
+
name = File.basename( wt.fetch( :path ) )
|
|
93
|
+
info = {
|
|
90
94
|
path: wt.fetch( :path ),
|
|
91
|
-
name:
|
|
95
|
+
name: name,
|
|
92
96
|
branch: wt.fetch( :branch, nil )
|
|
93
97
|
}
|
|
98
|
+
owner = ownership[ name ]
|
|
99
|
+
if owner
|
|
100
|
+
info[ :owner ] = owner[ :session_id ]
|
|
101
|
+
info[ :owner_pid ] = owner[ :pid ]
|
|
102
|
+
info[ :owner_task ] = owner[ :task ]
|
|
103
|
+
info[ :stale ] = owner[ :stale ]
|
|
104
|
+
end
|
|
105
|
+
info
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds a name-to-session mapping for worktree ownership.
|
|
110
|
+
def build_worktree_ownership( sessions: )
|
|
111
|
+
result = {}
|
|
112
|
+
sessions.each do |session|
|
|
113
|
+
wt = session[ :worktree ]
|
|
114
|
+
next unless wt
|
|
115
|
+
name = wt[ :name ] || wt[ "name" ]
|
|
116
|
+
next unless name
|
|
117
|
+
result[ name ] = {
|
|
118
|
+
session_id: session[ :session_id ] || session[ "session_id" ],
|
|
119
|
+
pid: session[ :pid ] || session[ "pid" ],
|
|
120
|
+
task: session[ :task ] || session[ "task" ],
|
|
121
|
+
stale: session[ :stale ]
|
|
122
|
+
}
|
|
94
123
|
end
|
|
124
|
+
result
|
|
95
125
|
end
|
|
96
126
|
|
|
97
127
|
# Queries open PRs via gh.
|
|
@@ -177,7 +207,8 @@ module Carson
|
|
|
177
207
|
puts_line "Worktrees:"
|
|
178
208
|
worktrees.each do |wt|
|
|
179
209
|
branch_label = wt.fetch( :branch ) || "(detached)"
|
|
180
|
-
|
|
210
|
+
owner_label = format_worktree_owner( worktree: wt )
|
|
211
|
+
puts_line " #{wt.fetch( :name )} #{branch_label}#{owner_label}"
|
|
181
212
|
end
|
|
182
213
|
end
|
|
183
214
|
|
|
@@ -211,6 +242,24 @@ module Carson
|
|
|
211
242
|
end
|
|
212
243
|
end
|
|
213
244
|
|
|
245
|
+
# Formats owner annotation for a worktree entry.
|
|
246
|
+
def format_worktree_owner( worktree: )
|
|
247
|
+
owner = worktree[ :owner ]
|
|
248
|
+
return "" unless owner
|
|
249
|
+
|
|
250
|
+
stale = worktree[ :stale ]
|
|
251
|
+
task = worktree[ :owner_task ]
|
|
252
|
+
pid = worktree[ :owner_pid ]
|
|
253
|
+
|
|
254
|
+
if stale
|
|
255
|
+
" (stale session #{pid})"
|
|
256
|
+
elsif task
|
|
257
|
+
" (#{task})"
|
|
258
|
+
else
|
|
259
|
+
" (session #{pid})"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
214
263
|
# Formats sync status for display.
|
|
215
264
|
def format_sync( sync: )
|
|
216
265
|
case sync
|