@hybridlabor-api/bdb-synapse 1.0.5 → 1.1.1
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.
- package/README.md +14 -0
- package/bin/synapse +0 -0
- package/bin/synapse-darwin-arm64 +0 -0
- package/bin/synapse-linux-amd64 +0 -0
- package/bin/synapse.exe +0 -0
- package/go.mod +14 -2
- package/go.sum +20 -0
- package/internal/adapter/agy/adapter.go +237 -10
- package/internal/adapter/opencode/adapter.go +304 -0
- package/internal/server/codex_index_test.go +1 -1
- package/internal/server/server.go +29 -3
- package/internal/server/server_test.go +13 -13
- package/internal/server/static/assets/index-ndrpq0ER.js +3 -0
- package/internal/server/static/index.html +2 -2
- package/package.json +2 -2
- package/web/dist/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/web/dist/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/web/dist/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/web/dist/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/web/dist/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/web/dist/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/web/dist/assets/index-BNoY_BiB.css +1 -0
- package/web/dist/assets/index-ndrpq0ER.js +3 -0
- package/web/dist/assets/index-ndrpq0ER.js.map +1 -0
- package/web/dist/assets/react-gcHzaSmV.js +10 -0
- package/web/dist/assets/react-gcHzaSmV.js.map +1 -0
- package/web/dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/web/dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/web/dist/assets/three-DnGjZfD1.js +4012 -0
- package/web/dist/assets/three-DnGjZfD1.js.map +1 -0
- package/web/dist/index.html +26 -0
- package/web/src/state/store.ts +1 -1
- package/web/tsconfig.tsbuildinfo +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +0 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
package opencode
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"database/sql"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter"
|
|
12
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/model"
|
|
13
|
+
_ "modernc.org/sqlite"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type Adapter struct {
|
|
17
|
+
Dir string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func DefaultDir() string {
|
|
21
|
+
home, err := os.UserHomeDir()
|
|
22
|
+
if err != nil {
|
|
23
|
+
return ""
|
|
24
|
+
}
|
|
25
|
+
return filepath.Join(home, ".local", "share", "opencode")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func (a Adapter) Harness() string { return "opencode" }
|
|
29
|
+
|
|
30
|
+
func (a Adapter) SessionDir() string {
|
|
31
|
+
if a.Dir != "" {
|
|
32
|
+
return a.Dir
|
|
33
|
+
}
|
|
34
|
+
return DefaultDir()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (a Adapter) dbPath() string {
|
|
38
|
+
return filepath.Join(a.SessionDir(), "opencode.db")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func (a Adapter) openDB() (*sql.DB, error) {
|
|
42
|
+
dbPath := a.dbPath()
|
|
43
|
+
if _, err := os.Stat(dbPath); err != nil {
|
|
44
|
+
return nil, err
|
|
45
|
+
}
|
|
46
|
+
return sql.Open("sqlite", "file:"+dbPath+"?mode=ro")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (a Adapter) ListSessions() ([]model.SessionMeta, error) {
|
|
50
|
+
db, err := a.openDB()
|
|
51
|
+
if err != nil {
|
|
52
|
+
return nil, nil
|
|
53
|
+
}
|
|
54
|
+
defer db.Close()
|
|
55
|
+
|
|
56
|
+
rows, err := db.Query(`
|
|
57
|
+
SELECT id, title, directory, parent_id, time_created, time_updated
|
|
58
|
+
FROM session
|
|
59
|
+
ORDER BY time_updated DESC
|
|
60
|
+
`)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return nil, err
|
|
63
|
+
}
|
|
64
|
+
defer rows.Close()
|
|
65
|
+
|
|
66
|
+
var metas []model.SessionMeta
|
|
67
|
+
for rows.Next() {
|
|
68
|
+
var (
|
|
69
|
+
id string
|
|
70
|
+
title string
|
|
71
|
+
directory string
|
|
72
|
+
parentID sql.NullString
|
|
73
|
+
timeCreated int64
|
|
74
|
+
timeUpdated int64
|
|
75
|
+
)
|
|
76
|
+
if err := rows.Scan(&id, &title, &directory, &parentID, &timeCreated, &timeUpdated); err != nil {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
startedAt := time.UnixMilli(timeCreated).UTC().Format(time.RFC3339)
|
|
81
|
+
endedAt := time.UnixMilli(timeUpdated).UTC().Format(time.RFC3339)
|
|
82
|
+
sessionPath := filepath.Join(a.SessionDir(), id)
|
|
83
|
+
|
|
84
|
+
meta := model.SessionMeta{
|
|
85
|
+
Key: adapter.SessionKey(a.Harness(), sessionPath),
|
|
86
|
+
ID: id,
|
|
87
|
+
Harness: "opencode",
|
|
88
|
+
Path: sessionPath,
|
|
89
|
+
Title: title,
|
|
90
|
+
Cwd: directory,
|
|
91
|
+
StartedAt: startedAt,
|
|
92
|
+
EndedAt: endedAt,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if parentID.Valid && parentID.String != "" {
|
|
96
|
+
meta.Auxiliary = true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var eventCount, userTurns int
|
|
100
|
+
_ = db.QueryRow(`
|
|
101
|
+
SELECT
|
|
102
|
+
COUNT(CASE WHEN data LIKE '%"type":"tool"%' THEN 1 END),
|
|
103
|
+
COUNT(CASE WHEN data LIKE '%"type":"text"%' THEN 1 END)
|
|
104
|
+
FROM part
|
|
105
|
+
WHERE session_id = ?
|
|
106
|
+
`, id).Scan(&eventCount, &userTurns)
|
|
107
|
+
|
|
108
|
+
meta.EventCount = eventCount
|
|
109
|
+
meta.UserTurns = userTurns
|
|
110
|
+
|
|
111
|
+
metas = append(metas, meta)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
sort.Slice(metas, func(i, j int) bool {
|
|
115
|
+
return metas[i].EndedAt > metas[j].EndedAt
|
|
116
|
+
})
|
|
117
|
+
return metas, nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func (a Adapter) SummaryInputs(path string) []string {
|
|
121
|
+
return []string{a.dbPath()}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func (a Adapter) Summarize(path string) (model.SessionMeta, error) {
|
|
125
|
+
id := filepath.Base(path)
|
|
126
|
+
db, err := a.openDB()
|
|
127
|
+
if err != nil {
|
|
128
|
+
return model.SessionMeta{}, err
|
|
129
|
+
}
|
|
130
|
+
defer db.Close()
|
|
131
|
+
|
|
132
|
+
var (
|
|
133
|
+
title string
|
|
134
|
+
directory string
|
|
135
|
+
parentID sql.NullString
|
|
136
|
+
timeCreated int64
|
|
137
|
+
timeUpdated int64
|
|
138
|
+
)
|
|
139
|
+
err = db.QueryRow(`
|
|
140
|
+
SELECT title, directory, parent_id, time_created, time_updated
|
|
141
|
+
FROM session
|
|
142
|
+
WHERE id = ?
|
|
143
|
+
`, id).Scan(&title, &directory, &parentID, &timeCreated, &timeUpdated)
|
|
144
|
+
if err != nil {
|
|
145
|
+
return model.SessionMeta{}, err
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
startedAt := time.UnixMilli(timeCreated).UTC().Format(time.RFC3339)
|
|
149
|
+
endedAt := time.UnixMilli(timeUpdated).UTC().Format(time.RFC3339)
|
|
150
|
+
|
|
151
|
+
meta := model.SessionMeta{
|
|
152
|
+
Key: adapter.SessionKey(a.Harness(), path),
|
|
153
|
+
ID: id,
|
|
154
|
+
Harness: "opencode",
|
|
155
|
+
Path: path,
|
|
156
|
+
Title: title,
|
|
157
|
+
Cwd: directory,
|
|
158
|
+
StartedAt: startedAt,
|
|
159
|
+
EndedAt: endedAt,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if parentID.Valid && parentID.String != "" {
|
|
163
|
+
meta.Auxiliary = true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var eventCount, userTurns int
|
|
167
|
+
_ = db.QueryRow(`
|
|
168
|
+
SELECT
|
|
169
|
+
COUNT(CASE WHEN data LIKE '%"type":"tool"%' THEN 1 END),
|
|
170
|
+
COUNT(CASE WHEN data LIKE '%"type":"text"%' THEN 1 END)
|
|
171
|
+
FROM part
|
|
172
|
+
WHERE session_id = ?
|
|
173
|
+
`, id).Scan(&eventCount, &userTurns)
|
|
174
|
+
|
|
175
|
+
meta.EventCount = eventCount
|
|
176
|
+
meta.UserTurns = userTurns
|
|
177
|
+
return meta, nil
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type toolPart struct {
|
|
181
|
+
Type string `json:"type"`
|
|
182
|
+
Tool string `json:"tool"`
|
|
183
|
+
CallID string `json:"callID"`
|
|
184
|
+
State struct {
|
|
185
|
+
Status string `json:"status"`
|
|
186
|
+
Input map[string]any `json:"input"`
|
|
187
|
+
Output any `json:"output"`
|
|
188
|
+
} `json:"state"`
|
|
189
|
+
Title string `json:"title"`
|
|
190
|
+
Time struct {
|
|
191
|
+
Start int64 `json:"start"`
|
|
192
|
+
End int64 `json:"end"`
|
|
193
|
+
} `json:"time"`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type textPart struct {
|
|
197
|
+
Type string `json:"type"`
|
|
198
|
+
Text string `json:"text"`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func (a Adapter) Parse(path string) (*model.Trace, error) {
|
|
202
|
+
id := filepath.Base(path)
|
|
203
|
+
db, err := a.openDB()
|
|
204
|
+
if err != nil {
|
|
205
|
+
return nil, err
|
|
206
|
+
}
|
|
207
|
+
defer db.Close()
|
|
208
|
+
|
|
209
|
+
var (
|
|
210
|
+
title string
|
|
211
|
+
directory string
|
|
212
|
+
timeCreated int64
|
|
213
|
+
timeUpdated int64
|
|
214
|
+
)
|
|
215
|
+
err = db.QueryRow(`
|
|
216
|
+
SELECT title, directory, time_created, time_updated
|
|
217
|
+
FROM session
|
|
218
|
+
WHERE id = ?
|
|
219
|
+
`, id).Scan(&title, &directory, &timeCreated, &timeUpdated)
|
|
220
|
+
if err != nil {
|
|
221
|
+
return nil, err
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
startedAt := time.UnixMilli(timeCreated).UTC().Format(time.RFC3339)
|
|
225
|
+
endedAt := time.UnixMilli(timeUpdated).UTC().Format(time.RFC3339)
|
|
226
|
+
|
|
227
|
+
trace := &model.Trace{
|
|
228
|
+
Version: 1,
|
|
229
|
+
Session: model.TraceSession{
|
|
230
|
+
ID: id,
|
|
231
|
+
Harness: "opencode",
|
|
232
|
+
Path: path,
|
|
233
|
+
Title: title,
|
|
234
|
+
Cwd: directory,
|
|
235
|
+
StartedAt: startedAt,
|
|
236
|
+
EndedAt: endedAt,
|
|
237
|
+
},
|
|
238
|
+
Events: []model.Event{},
|
|
239
|
+
Marks: []model.Mark{},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
rows, err := db.Query(`
|
|
243
|
+
SELECT data, time_created
|
|
244
|
+
FROM part
|
|
245
|
+
WHERE session_id = ?
|
|
246
|
+
ORDER BY time_created ASC, id ASC
|
|
247
|
+
`, id)
|
|
248
|
+
if err != nil {
|
|
249
|
+
return trace, nil
|
|
250
|
+
}
|
|
251
|
+
defer rows.Close()
|
|
252
|
+
|
|
253
|
+
for rows.Next() {
|
|
254
|
+
var rawData string
|
|
255
|
+
var partTime int64
|
|
256
|
+
if err := rows.Scan(&rawData, &partTime); err != nil {
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var tp toolPart
|
|
261
|
+
if err := json.Unmarshal([]byte(rawData), &tp); err == nil && tp.Type == "tool" {
|
|
262
|
+
ts := time.UnixMilli(partTime).UTC().Format(time.RFC3339)
|
|
263
|
+
call := adapter.ToolCall{
|
|
264
|
+
ID: tp.CallID,
|
|
265
|
+
Name: tp.Tool,
|
|
266
|
+
Input: tp.State.Input,
|
|
267
|
+
Timestamp: ts,
|
|
268
|
+
}
|
|
269
|
+
outStr := ""
|
|
270
|
+
if s, ok := tp.State.Output.(string); ok {
|
|
271
|
+
outStr = s
|
|
272
|
+
} else if tp.State.Output != nil {
|
|
273
|
+
b, _ := json.Marshal(tp.State.Output)
|
|
274
|
+
outStr = string(b)
|
|
275
|
+
}
|
|
276
|
+
result := adapter.ToolResult{
|
|
277
|
+
Content: outStr,
|
|
278
|
+
OutcomeKnown: tp.State.Status != "",
|
|
279
|
+
IsError: tp.State.Status == "error",
|
|
280
|
+
}
|
|
281
|
+
event := adapter.BuildEvent(trace, call, result)
|
|
282
|
+
trace.Events = append(trace.Events, event)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var txt textPart
|
|
287
|
+
if err := json.Unmarshal([]byte(rawData), &txt); err == nil && txt.Type == "text" && txt.Text != "" {
|
|
288
|
+
if !adapter.InjectedUserMessage(txt.Text) {
|
|
289
|
+
trace.Marks = append(trace.Marks, model.Mark{
|
|
290
|
+
Seq: len(trace.Events),
|
|
291
|
+
Type: "user-message",
|
|
292
|
+
Note: adapter.UserMessageNote(txt.Text),
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for i := range trace.Events {
|
|
299
|
+
trace.Events[i].Seq = i
|
|
300
|
+
}
|
|
301
|
+
trace.Session.EventCount = len(trace.Events)
|
|
302
|
+
trace.Stats = model.ComputeStats(trace, 0, model.ObservabilityExact)
|
|
303
|
+
return trace, nil
|
|
304
|
+
}
|
|
@@ -26,7 +26,7 @@ func TestFreshScanPicksUpCodexTitleIndexChange(t *testing.T) {
|
|
|
26
26
|
}
|
|
27
27
|
writeIndex("Title One")
|
|
28
28
|
|
|
29
|
-
s := New(Config{ClaudeDir: filepath.Join(t.TempDir(), "claude"), CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
29
|
+
s := New(Config{ClaudeDir: filepath.Join(t.TempDir(), "claude"), CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
30
30
|
initial := requestSessions(t, s, "/api/sessions")
|
|
31
31
|
if len(initial) != 1 || initial[0].Title != "Title One" {
|
|
32
32
|
t.Fatalf("initial sessions = %#v", initial)
|
|
@@ -26,6 +26,7 @@ import (
|
|
|
26
26
|
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/claudecode"
|
|
27
27
|
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/codex"
|
|
28
28
|
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/agy"
|
|
29
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/opencode"
|
|
29
30
|
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/pi"
|
|
30
31
|
"github.com/hybridlabor-api/bdb-synapse/internal/citymap"
|
|
31
32
|
"github.com/hybridlabor-api/bdb-synapse/internal/judge"
|
|
@@ -41,6 +42,7 @@ type Config struct {
|
|
|
41
42
|
CodexDir string
|
|
42
43
|
PiDir string
|
|
43
44
|
AgyDir string
|
|
45
|
+
OpencodeDir string
|
|
44
46
|
OpenSession string
|
|
45
47
|
Dev bool
|
|
46
48
|
RepoRoot string
|
|
@@ -122,7 +124,7 @@ const (
|
|
|
122
124
|
func New(cfg Config) *Server {
|
|
123
125
|
s := &Server{
|
|
124
126
|
cfg: cfg,
|
|
125
|
-
adapters: []adapter.Source{claudecode.Adapter{Dir: cfg.ClaudeDir}, codex.Adapter{Dir: cfg.CodexDir}, pi.Adapter{Dir: cfg.PiDir}, agy.Adapter{Dir: cfg.AgyDir}},
|
|
127
|
+
adapters: []adapter.Source{claudecode.Adapter{Dir: cfg.ClaudeDir}, codex.Adapter{Dir: cfg.CodexDir}, pi.Adapter{Dir: cfg.PiDir}, agy.Adapter{Dir: cfg.AgyDir}, opencode.Adapter{Dir: cfg.OpencodeDir}},
|
|
126
128
|
agentGraphs: map[string]agentGraphCacheEntry{},
|
|
127
129
|
agentGraphLoads: map[string]*inflightAgentGraph{},
|
|
128
130
|
summaries: map[string]summaryCacheEntry{},
|
|
@@ -540,7 +542,14 @@ func (s *Server) scanSessions() ([]model.SessionMeta, error) {
|
|
|
540
542
|
}
|
|
541
543
|
seen := map[string]bool{}
|
|
542
544
|
var files []sessionFile
|
|
545
|
+
var directSessions []model.SessionMeta
|
|
543
546
|
for _, source := range s.adapters {
|
|
547
|
+
if source.Harness() == "opencode" {
|
|
548
|
+
if list, err := source.ListSessions(); err == nil {
|
|
549
|
+
directSessions = append(directSessions, list...)
|
|
550
|
+
}
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
544
553
|
dir := source.SessionDir()
|
|
545
554
|
if dir == "" {
|
|
546
555
|
continue
|
|
@@ -558,6 +567,11 @@ func (s *Server) scanSessions() ([]model.SessionMeta, error) {
|
|
|
558
567
|
if filepath.Ext(path) != ".jsonl" {
|
|
559
568
|
return nil
|
|
560
569
|
}
|
|
570
|
+
if source.Harness() == "agy" {
|
|
571
|
+
if filepath.Base(path) != "transcript.jsonl" || strings.Contains(path, "/chunks/") {
|
|
572
|
+
return nil
|
|
573
|
+
}
|
|
574
|
+
}
|
|
561
575
|
info, err := entry.Info()
|
|
562
576
|
if err != nil {
|
|
563
577
|
return nil
|
|
@@ -605,8 +619,8 @@ func (s *Server) scanSessions() ([]model.SessionMeta, error) {
|
|
|
605
619
|
}
|
|
606
620
|
}
|
|
607
621
|
|
|
608
|
-
catalog := make(map[string]model.SessionMeta, len(files))
|
|
609
|
-
sessions := make([]model.SessionMeta, 0, len(files))
|
|
622
|
+
catalog := make(map[string]model.SessionMeta, len(files)+len(directSessions))
|
|
623
|
+
sessions := make([]model.SessionMeta, 0, len(files)+len(directSessions))
|
|
610
624
|
for _, meta := range results {
|
|
611
625
|
if meta == nil {
|
|
612
626
|
continue
|
|
@@ -616,6 +630,15 @@ func (s *Server) scanSessions() ([]model.SessionMeta, error) {
|
|
|
616
630
|
sessions = append(sessions, *meta)
|
|
617
631
|
}
|
|
618
632
|
}
|
|
633
|
+
for _, meta := range directSessions {
|
|
634
|
+
catalog[meta.Key] = meta
|
|
635
|
+
if !meta.Auxiliary {
|
|
636
|
+
sessions = append(sessions, meta)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
sort.Slice(sessions, func(i, j int) bool {
|
|
640
|
+
return sessions[i].EndedAt > sessions[j].EndedAt
|
|
641
|
+
})
|
|
619
642
|
s.mu.Lock()
|
|
620
643
|
s.sessionCatalog = catalog
|
|
621
644
|
s.mu.Unlock()
|
|
@@ -967,6 +990,9 @@ func (s *Server) findSession(selector string) (model.SessionMeta, error) {
|
|
|
967
990
|
func fingerprintFile(path string) (fileFingerprint, error) {
|
|
968
991
|
info, err := os.Stat(path)
|
|
969
992
|
if err != nil {
|
|
993
|
+
if dbInfo, dbErr := os.Stat(filepath.Join(filepath.Dir(path), "opencode.db")); dbErr == nil {
|
|
994
|
+
return fileFingerprint{size: dbInfo.Size(), modTime: dbInfo.ModTime()}, nil
|
|
995
|
+
}
|
|
970
996
|
return fileFingerprint{}, err
|
|
971
997
|
}
|
|
972
998
|
return fileFingerprint{size: info.Size(), modTime: info.ModTime()}, nil
|
|
@@ -28,7 +28,7 @@ func TestTraceStillLoadsWhenSessionCwdIsMissing(t *testing.T) {
|
|
|
28
28
|
`{"type":"user","timestamp":"2026-07-09T00:00:02Z","sessionId":"missingcwd","cwd":`+quoteJSON(missingRoot)+`,"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"r1","content":"ok","is_error":false}]}}`,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
31
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
32
32
|
traceResp := httptest.NewRecorder()
|
|
33
33
|
s.handleSessionResource(traceResp, httptest.NewRequest(http.MethodGet, "/api/sessions/missingcwd/trace", nil))
|
|
34
34
|
if traceResp.Code != http.StatusOK {
|
|
@@ -79,7 +79,7 @@ func TestOpenSessionUsesUniqueKeyAndFindSessionAcceptsBasename(t *testing.T) {
|
|
|
79
79
|
`{"type":"user","timestamp":"2026-07-09T00:00:00Z","sessionId":"internal-id","cwd":"/tmp","message":{"role":"user","content":"hello"}}`,
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), OpenSession: session})
|
|
82
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode"), OpenSession: session})
|
|
83
83
|
wantKey := adapter.SessionKey("claude-code", session)
|
|
84
84
|
if got := s.openSessionKey(); got != wantKey {
|
|
85
85
|
t.Fatalf("openSessionKey = %q, want %q", got, wantKey)
|
|
@@ -110,7 +110,7 @@ func TestDuplicateSessionIDsUseDistinctKeysAndCaches(t *testing.T) {
|
|
|
110
110
|
})
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
113
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
114
114
|
sessions, err := s.listSessions()
|
|
115
115
|
if err != nil {
|
|
116
116
|
t.Fatal(err)
|
|
@@ -158,7 +158,7 @@ func TestTraceCacheReloadsWhenActiveSessionGrows(t *testing.T) {
|
|
|
158
158
|
`{"type":"user","timestamp":"2026-07-09T00:00:02Z","sessionId":"growing","cwd":`+quoteJSON(repoRoot)+`,"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"r1","content":"ok","is_error":false}]}}`,
|
|
159
159
|
)
|
|
160
160
|
|
|
161
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
161
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
162
162
|
firstTrace, firstCity, err := s.traceAndMap("growing")
|
|
163
163
|
if err != nil {
|
|
164
164
|
t.Fatal(err)
|
|
@@ -192,7 +192,7 @@ func TestSessionsFreshBypassesListTTL(t *testing.T) {
|
|
|
192
192
|
writeServerSession(t, filepath.Join(claudeDir, "first.jsonl"),
|
|
193
193
|
`{"type":"user","timestamp":"2026-07-09T00:00:00Z","sessionId":"first","cwd":"/tmp","message":{"role":"user","content":"hello"}}`,
|
|
194
194
|
)
|
|
195
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
195
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
196
196
|
|
|
197
197
|
initial := requestSessions(t, s, "/api/sessions")
|
|
198
198
|
if len(initial) != 1 {
|
|
@@ -217,7 +217,7 @@ func TestConcurrentFreshGenerationReusesCompletedScan(t *testing.T) {
|
|
|
217
217
|
writeServerSession(t, filepath.Join(claudeDir, "session.jsonl"),
|
|
218
218
|
`{"type":"user","timestamp":"2026-07-09T00:00:00Z","sessionId":"session","cwd":"/tmp","message":{"role":"user","content":"hello"}}`,
|
|
219
219
|
)
|
|
220
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
220
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
221
221
|
|
|
222
222
|
// Two fresh callers that enter before either scan completes observe the
|
|
223
223
|
// same generation. The second must reuse the first completed scan.
|
|
@@ -389,7 +389,7 @@ func TestServerLoadsCodexSessions(t *testing.T) {
|
|
|
389
389
|
},
|
|
390
390
|
)
|
|
391
391
|
|
|
392
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
392
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
393
393
|
sessionsResp := httptest.NewRecorder()
|
|
394
394
|
s.handleSessions(sessionsResp, httptest.NewRequest(http.MethodGet, "/api/sessions", nil))
|
|
395
395
|
if sessionsResp.Code != http.StatusOK {
|
|
@@ -478,7 +478,7 @@ func TestCodexAgentAPIsDoNotDeriveChildAcrossDuplicateRootIDs(t *testing.T) {
|
|
|
478
478
|
},
|
|
479
479
|
})
|
|
480
480
|
|
|
481
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
481
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
482
482
|
sessions := requestSessions(t, s, "/api/sessions")
|
|
483
483
|
if len(sessions) != 2 {
|
|
484
484
|
t.Fatalf("visible sessions = %#v, want duplicate-ID roots only", sessions)
|
|
@@ -537,7 +537,7 @@ func TestServerRetainsClaudeSubagentInCatalog(t *testing.T) {
|
|
|
537
537
|
`{"type":"user","timestamp":"2026-07-09T00:00:01Z","sessionId":"root-id","agentId":"child","isSidechain":true,"cwd":"/tmp","message":{"role":"user","content":"internal"}}`,
|
|
538
538
|
)
|
|
539
539
|
|
|
540
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
540
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
541
541
|
sessions := requestSessions(t, s, "/api/sessions")
|
|
542
542
|
if len(sessions) != 1 || sessions[0].ID != "root-id" {
|
|
543
543
|
t.Fatalf("sessions = %#v", sessions)
|
|
@@ -857,7 +857,7 @@ func TestFreshScanReloadsClaudeSidecarMetadata(t *testing.T) {
|
|
|
857
857
|
`{"type":"user","timestamp":"2026-07-09T00:00:01Z","sessionId":"root-id","agentId":"child","isSidechain":true,"cwd":"/tmp","message":{"role":"user","content":"internal"}}`,
|
|
858
858
|
)
|
|
859
859
|
|
|
860
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
860
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
861
861
|
requestSessions(t, s, "/api/sessions")
|
|
862
862
|
childKey := adapter.SessionKey("claude-code", child)
|
|
863
863
|
if got := s.sessionCatalog[childKey].Agent.Role; got != "" {
|
|
@@ -914,7 +914,7 @@ func TestServerSkipsCodexSubagentSessions(t *testing.T) {
|
|
|
914
914
|
},
|
|
915
915
|
)
|
|
916
916
|
|
|
917
|
-
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi")})
|
|
917
|
+
s := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
918
918
|
sessions, err := s.scanSessions()
|
|
919
919
|
if err != nil {
|
|
920
920
|
t.Fatal(err)
|
|
@@ -925,7 +925,7 @@ func TestServerSkipsCodexSubagentSessions(t *testing.T) {
|
|
|
925
925
|
|
|
926
926
|
// Explicitly opening an auxiliary rollout keeps it available internally
|
|
927
927
|
// without leaking it into the visible session list.
|
|
928
|
-
explicit := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), OpenSession: subagentSession})
|
|
928
|
+
explicit := New(Config{ClaudeDir: claudeDir, CodexDir: codexDir, PiDir: filepath.Join(t.TempDir(), "pi"), AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode"), OpenSession: subagentSession})
|
|
929
929
|
explicitSessions, err := explicit.listSessions()
|
|
930
930
|
if err != nil {
|
|
931
931
|
t.Fatal(err)
|
|
@@ -1362,7 +1362,7 @@ func TestSessionAgentsForSourceWithoutGraphSupport(t *testing.T) {
|
|
|
1362
1362
|
t.Fatal(err)
|
|
1363
1363
|
}
|
|
1364
1364
|
|
|
1365
|
-
s := New(Config{ClaudeDir: filepath.Join(t.TempDir(), "claude"), CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: piRoot})
|
|
1365
|
+
s := New(Config{ClaudeDir: filepath.Join(t.TempDir(), "claude"), CodexDir: filepath.Join(t.TempDir(), "codex"), PiDir: piRoot, AgyDir: filepath.Join(t.TempDir(), "agy"), OpencodeDir: filepath.Join(t.TempDir(), "opencode")})
|
|
1366
1366
|
if sessions := requestSessions(t, s, "/api/sessions"); len(sessions) != 1 {
|
|
1367
1367
|
t.Fatalf("sessions = %#v", sessions)
|
|
1368
1368
|
}
|