@hybridlabor-api/bdb-synapse 1.0.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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,919 @@
1
+ package codex
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "regexp"
9
+ "sort"
10
+ "strings"
11
+ "sync"
12
+ "time"
13
+
14
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter"
15
+ "github.com/hybridlabor-api/bdb-synapse/internal/judge"
16
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
17
+ )
18
+
19
+ type Adapter struct {
20
+ Dir string
21
+ IndexPath string
22
+ }
23
+
24
+ func DefaultDir() string {
25
+ home, err := os.UserHomeDir()
26
+ if err != nil {
27
+ return ""
28
+ }
29
+ return filepath.Join(home, ".codex", "sessions")
30
+ }
31
+
32
+ func DefaultIndexPath() string {
33
+ home, err := os.UserHomeDir()
34
+ if err != nil {
35
+ return ""
36
+ }
37
+ return filepath.Join(home, ".codex", "session_index.jsonl")
38
+ }
39
+
40
+ func (a Adapter) Harness() string {
41
+ return "codex"
42
+ }
43
+
44
+ func (a Adapter) SessionDir() string {
45
+ if a.Dir != "" {
46
+ return a.Dir
47
+ }
48
+ return DefaultDir()
49
+ }
50
+
51
+ func (a Adapter) ListSessions() ([]model.SessionMeta, error) {
52
+ dir := a.SessionDir()
53
+ if info, err := os.Stat(dir); err != nil || !info.IsDir() {
54
+ return nil, nil
55
+ }
56
+ var metas []model.SessionMeta
57
+ err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, walkErr error) error {
58
+ if walkErr != nil {
59
+ return nil
60
+ }
61
+ if entry.IsDir() {
62
+ return nil
63
+ }
64
+ if filepath.Ext(path) != ".jsonl" {
65
+ return nil
66
+ }
67
+ meta, err := a.Summarize(path)
68
+ if err == nil && !meta.Auxiliary {
69
+ metas = append(metas, meta)
70
+ }
71
+ return nil
72
+ })
73
+ if err != nil {
74
+ return nil, err
75
+ }
76
+ sort.Slice(metas, func(i, j int) bool {
77
+ return metas[i].EndedAt > metas[j].EndedAt
78
+ })
79
+ return metas, nil
80
+ }
81
+
82
+ // SummaryInputs reports the sidecar files Summarize reads for a session:
83
+ // titles come from a session_index.jsonl resolved next to the sessions
84
+ // directory or at the default location, so an index change — a Codex thread
85
+ // rename — must invalidate cached summaries. Candidates are declared even
86
+ // while missing (unlike indexPath, which stat-gates), so creating the index
87
+ // later invalidates too.
88
+ func (a Adapter) SummaryInputs(_ string) []string {
89
+ if a.IndexPath != "" {
90
+ return []string{a.IndexPath}
91
+ }
92
+ var inputs []string
93
+ if dir := filepath.Clean(a.SessionDir()); dir != "" && dir != "." {
94
+ inputs = append(inputs, filepath.Join(filepath.Dir(dir), "session_index.jsonl"))
95
+ }
96
+ if candidate := DefaultIndexPath(); candidate != "" && (len(inputs) == 0 || inputs[0] != candidate) {
97
+ inputs = append(inputs, candidate)
98
+ }
99
+ return inputs
100
+ }
101
+
102
+ func (a Adapter) Summarize(path string) (model.SessionMeta, error) {
103
+ f, err := os.Open(path)
104
+ if err != nil {
105
+ return model.SessionMeta{}, err
106
+ }
107
+ defer f.Close()
108
+
109
+ id := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
110
+ meta := model.SessionMeta{Key: adapter.SessionKey(a.Harness(), path), ID: id, Harness: a.Harness(), Path: path}
111
+ recognized := false
112
+ sawSessionMeta := false
113
+ seenCalls := map[string]bool{}
114
+ err = adapter.ReadJSONLines(f, func(data []byte) {
115
+ var line rawLine
116
+ if json.Unmarshal(data, &line) != nil {
117
+ return
118
+ }
119
+ if line.Timestamp != "" {
120
+ if meta.StartedAt == "" {
121
+ meta.StartedAt = line.Timestamp
122
+ }
123
+ meta.EndedAt = line.Timestamp
124
+ }
125
+ switch line.Type {
126
+ case "session_meta":
127
+ recognized = true
128
+ var payload sessionMetaPayload
129
+ if json.Unmarshal(line.Payload, &payload) == nil {
130
+ firstSessionMeta := !sawSessionMeta
131
+ if !sawSessionMeta {
132
+ sawSessionMeta = true
133
+ meta.Auxiliary = payload.isSubagent()
134
+ }
135
+ applySessionMeta(&meta, payload, firstSessionMeta)
136
+ }
137
+ case "turn_context":
138
+ recognized = true
139
+ var payload turnContextPayload
140
+ if json.Unmarshal(line.Payload, &payload) == nil {
141
+ if payload.Cwd != "" && meta.Cwd == "" {
142
+ meta.Cwd = payload.Cwd
143
+ }
144
+ if payload.Model != "" && meta.Model == "" {
145
+ meta.Model = payload.Model
146
+ }
147
+ }
148
+ case "response_item":
149
+ recognized = true
150
+ var payload responseItemHeader
151
+ if json.Unmarshal(line.Payload, &payload) == nil {
152
+ if callID, ok := canonicalCallID(payload.Type, payload.ID, payload.CallID, payload.Name); ok && !seenCalls[callID] {
153
+ seenCalls[callID] = true
154
+ meta.EventCount++
155
+ }
156
+ // only message items pay the full decode; the count must
157
+ // mirror Parse's user-message mark filter
158
+ if payload.Type == "message" {
159
+ var msg responseItemPayload
160
+ if json.Unmarshal(line.Payload, &msg) == nil && msg.Role == "user" && msg.Content.HasText() {
161
+ if !adapter.InjectedUserMessage(msg.Content.Text()) {
162
+ meta.UserTurns++
163
+ }
164
+ }
165
+ }
166
+ }
167
+ case "event_msg":
168
+ recognized = true
169
+ case "message":
170
+ // Codex message lines carry the role at the top level; pi
171
+ // sessions also use type "message" but nest theirs, so a
172
+ // roleless line must not claim the file for this adapter.
173
+ if line.Role == "" {
174
+ return
175
+ }
176
+ recognized = true
177
+ if line.Role == "user" && line.Content.HasText() {
178
+ if !adapter.InjectedUserMessage(line.Content.Text()) {
179
+ meta.UserTurns++
180
+ }
181
+ }
182
+ case "":
183
+ if line.ID != "" {
184
+ recognized = true
185
+ meta.ID = line.ID
186
+ }
187
+ }
188
+ })
189
+ if judge.IsWorkDir(meta.Cwd) {
190
+ // Sessions recorded in the judge workdir are synapse's own judge
191
+ // runs (codex cannot disable session persistence), not user coding
192
+ // sessions; auxiliary keeps them out of every listing.
193
+ meta.Auxiliary = true
194
+ }
195
+ if meta.Title == "" {
196
+ meta.Title = a.titleFor(meta.ID)
197
+ }
198
+ if meta.Title == "" {
199
+ meta.Title = filepath.Base(path)
200
+ }
201
+ if !recognized {
202
+ return model.SessionMeta{}, fmt.Errorf("not a Codex session: %s", path)
203
+ }
204
+ return meta, err
205
+ }
206
+
207
+ func (a Adapter) Parse(path string) (*model.Trace, error) {
208
+ f, err := os.Open(path)
209
+ if err != nil {
210
+ return nil, err
211
+ }
212
+ defer f.Close()
213
+
214
+ id := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
215
+ trace := &model.Trace{
216
+ Version: 1,
217
+ Session: model.TraceSession{
218
+ ID: id,
219
+ Harness: a.Harness(),
220
+ Path: path,
221
+ },
222
+ Events: []model.Event{},
223
+ Marks: []model.Mark{},
224
+ }
225
+
226
+ recognized := false
227
+ sawSessionMeta := false
228
+ calls := map[string]adapter.ToolCall{}
229
+ callOrder := []string{}
230
+ results := map[string]adapter.ToolResult{}
231
+ directPatches := map[string]bool{}
232
+ patchResults := map[string]patchApplyEndPayload{}
233
+ err = adapter.ReadJSONLines(f, func(data []byte) {
234
+ var line rawLine
235
+ if json.Unmarshal(data, &line) != nil {
236
+ return
237
+ }
238
+ applyLineTime(trace, line.Timestamp)
239
+ switch line.Type {
240
+ case "session_meta":
241
+ recognized = true
242
+ var payload sessionMetaPayload
243
+ if json.Unmarshal(line.Payload, &payload) == nil {
244
+ firstSessionMeta := !sawSessionMeta
245
+ sawSessionMeta = true
246
+ applyTraceSessionMeta(trace, payload, firstSessionMeta)
247
+ }
248
+ case "turn_context":
249
+ recognized = true
250
+ var payload turnContextPayload
251
+ if json.Unmarshal(line.Payload, &payload) == nil {
252
+ if payload.Cwd != "" && trace.Session.Cwd == "" {
253
+ trace.Session.Cwd = payload.Cwd
254
+ }
255
+ if payload.Model != "" && trace.Session.Model == "" {
256
+ trace.Session.Model = payload.Model
257
+ }
258
+ }
259
+ case "response_item":
260
+ recognized = true
261
+ var payload responseItemPayload
262
+ if json.Unmarshal(line.Payload, &payload) != nil {
263
+ return
264
+ }
265
+ if call, source, ok := decodeCall(payload, line.Timestamp); ok {
266
+ if _, exists := calls[call.ID]; exists {
267
+ return
268
+ }
269
+ if call.Name == "spawn_agent" {
270
+ trace.Marks = append(trace.Marks, model.Mark{Seq: len(callOrder), Type: "subagent", Note: call.Name})
271
+ }
272
+ calls[call.ID] = call
273
+ callOrder = append(callOrder, call.ID)
274
+ directPatches[call.ID] = source == "custom_tool_call" && call.Name == "apply_patch"
275
+ return
276
+ }
277
+ if callID, result, ok := decodeOutput(payload); ok {
278
+ call, exists := calls[callID]
279
+ if !exists {
280
+ return
281
+ }
282
+ if _, exists := results[callID]; exists {
283
+ return
284
+ }
285
+ result.IsError, result.OutcomeKnown = toolOutputStatus(call.Name, result.Content)
286
+ results[callID] = result
287
+ return
288
+ }
289
+ if payload.Type == "message" {
290
+ if payload.Role == "user" && payload.Content.HasText() {
291
+ if text := payload.Content.Text(); !adapter.InjectedUserMessage(text) {
292
+ trace.Marks = append(trace.Marks, model.Mark{
293
+ Seq: len(callOrder),
294
+ Type: "user-message",
295
+ Note: adapter.UserMessageNote(text),
296
+ })
297
+ }
298
+ }
299
+ }
300
+ case "message":
301
+ // Mirrors Summarize: a roleless "message" line is another
302
+ // source's shape (pi nests the payload), not a Codex line.
303
+ if line.Role == "" {
304
+ return
305
+ }
306
+ recognized = true
307
+ if line.Role == "user" && line.Content.HasText() {
308
+ if text := line.Content.Text(); !adapter.InjectedUserMessage(text) {
309
+ trace.Marks = append(trace.Marks, model.Mark{
310
+ Seq: len(callOrder),
311
+ Type: "user-message",
312
+ Note: adapter.UserMessageNote(text),
313
+ })
314
+ }
315
+ }
316
+ case "event_msg":
317
+ recognized = true
318
+ var payload patchApplyEndPayload
319
+ if json.Unmarshal(line.Payload, &payload) != nil {
320
+ return
321
+ }
322
+ payload.Success = exactBoolPointer(line.Payload, "success")
323
+ if payload.Type == "context_compacted" {
324
+ trace.Marks = append(trace.Marks, model.Mark{Seq: len(callOrder), Type: "compaction"})
325
+ return
326
+ }
327
+ if payload.Type != "patch_apply_end" || payload.CallID == "" {
328
+ return
329
+ }
330
+ if !directPatches[payload.CallID] {
331
+ return
332
+ }
333
+ if _, exists := patchResults[payload.CallID]; !exists {
334
+ patchResults[payload.CallID] = payload
335
+ }
336
+ case "":
337
+ if line.ID != "" {
338
+ recognized = true
339
+ trace.Session.ID = line.ID
340
+ }
341
+ }
342
+ })
343
+ for _, id := range callOrder {
344
+ call := calls[id]
345
+ result := results[id]
346
+ if patchResult, ok := patchResults[id]; ok {
347
+ call.Input = applyPatchChanges(call.Input, patchResult.Changes)
348
+ if patchResult.Success != nil {
349
+ result.IsError = !*patchResult.Success
350
+ result.OutcomeKnown = true
351
+ }
352
+ }
353
+ trace.Events = append(trace.Events, adapter.BuildEvent(trace, call, result))
354
+ }
355
+ for i := range trace.Events {
356
+ trace.Events[i].Seq = i
357
+ }
358
+ if trace.Session.Title == "" {
359
+ trace.Session.Title = a.titleFor(trace.Session.ID)
360
+ }
361
+ if trace.Session.Title == "" {
362
+ trace.Session.Title = filepath.Base(path)
363
+ }
364
+ trace.Session.EventCount = len(trace.Events)
365
+ // Codex logs carry no structural error flag; failures are inferred from
366
+ // output text, and inner commands of the js exec runner surface only what
367
+ // the script chooses to print.
368
+ trace.Stats = model.ComputeStats(trace, 0, model.ObservabilityEstimated)
369
+ if !recognized {
370
+ return nil, fmt.Errorf("not a Codex session: %s", path)
371
+ }
372
+ return trace, err
373
+ }
374
+
375
+ type rawLine struct {
376
+ Type string `json:"type"`
377
+ Timestamp string `json:"timestamp"`
378
+ Payload json.RawMessage `json:"payload"`
379
+ ID string `json:"id"`
380
+ Role string `json:"role"`
381
+ Content codexContentList `json:"content"`
382
+ }
383
+
384
+ type sessionMetaPayload struct {
385
+ ID string `json:"id"`
386
+ SessionID string `json:"session_id"`
387
+ ParentThreadID string `json:"parent_thread_id"`
388
+ AgentPath string `json:"agent_path"`
389
+ AgentNickname string `json:"agent_nickname"`
390
+ AgentRole string `json:"agent_role"`
391
+ Timestamp string `json:"timestamp"`
392
+ Cwd string `json:"cwd"`
393
+ ThreadSource string `json:"thread_source"`
394
+ Source json.RawMessage `json:"source"`
395
+ Git struct {
396
+ Branch string `json:"branch"`
397
+ CommitHash string `json:"commit_hash"`
398
+ } `json:"git"`
399
+ }
400
+
401
+ func (p sessionMetaPayload) agentMeta() *model.AgentSessionMeta {
402
+ if !p.isSubagent() {
403
+ return nil
404
+ }
405
+ agent := &model.AgentSessionMeta{
406
+ SourceID: p.ID,
407
+ RootSessionID: p.SessionID,
408
+ ParentSessionID: p.ParentThreadID,
409
+ AgentPath: p.AgentPath,
410
+ Label: p.AgentNickname,
411
+ Role: p.AgentRole,
412
+ }
413
+ var source struct {
414
+ Subagent json.RawMessage `json:"subagent"`
415
+ }
416
+ var subagent struct {
417
+ ThreadSpawn struct {
418
+ ParentThreadID string `json:"parent_thread_id"`
419
+ Depth int `json:"depth"`
420
+ AgentPath string `json:"agent_path"`
421
+ AgentNickname string `json:"agent_nickname"`
422
+ AgentRole string `json:"agent_role"`
423
+ } `json:"thread_spawn"`
424
+ }
425
+ if json.Unmarshal(p.Source, &source) == nil && json.Unmarshal(source.Subagent, &subagent) == nil {
426
+ agent.Depth = subagent.ThreadSpawn.Depth
427
+ if agent.AgentPath == "" {
428
+ agent.AgentPath = subagent.ThreadSpawn.AgentPath
429
+ }
430
+ if agent.ParentSessionID == "" {
431
+ agent.ParentSessionID = subagent.ThreadSpawn.ParentThreadID
432
+ }
433
+ if agent.Label == "" {
434
+ agent.Label = subagent.ThreadSpawn.AgentNickname
435
+ }
436
+ if agent.Role == "" {
437
+ agent.Role = subagent.ThreadSpawn.AgentRole
438
+ }
439
+ }
440
+ return agent
441
+ }
442
+
443
+ func (p sessionMetaPayload) isSubagent() bool {
444
+ if p.ThreadSource == "subagent" {
445
+ return true
446
+ }
447
+ if len(p.Source) == 0 {
448
+ return false
449
+ }
450
+ var source struct {
451
+ Subagent json.RawMessage `json:"subagent"`
452
+ }
453
+ if json.Unmarshal(p.Source, &source) != nil {
454
+ return false
455
+ }
456
+ subagent := strings.TrimSpace(string(source.Subagent))
457
+ return subagent != "" && subagent != "null"
458
+ }
459
+
460
+ type turnContextPayload struct {
461
+ Cwd string `json:"cwd"`
462
+ Model string `json:"model"`
463
+ }
464
+
465
+ type responseItemPayload struct {
466
+ Type string `json:"type"`
467
+ ID string `json:"id"`
468
+ Name string `json:"name"`
469
+ Arguments json.RawMessage `json:"arguments"`
470
+ Input json.RawMessage `json:"input"`
471
+ CallID string `json:"call_id"`
472
+ Output any `json:"output"`
473
+ Role string `json:"role"`
474
+ Content codexContentList `json:"content"`
475
+ }
476
+
477
+ // responseItemHeader keeps session-list scans allocation-light. Summarize only
478
+ // needs call identity, so decoding large arguments, outputs, and patch bodies
479
+ // here would add substantial cold-start cost without changing the count.
480
+ type responseItemHeader struct {
481
+ Type string `json:"type"`
482
+ ID string `json:"id"`
483
+ Name string `json:"name"`
484
+ CallID string `json:"call_id"`
485
+ }
486
+
487
+ type patchApplyEndPayload struct {
488
+ Type string `json:"type"`
489
+ CallID string `json:"call_id"`
490
+ Success *bool `json:"success"`
491
+ Changes map[string]patchApplyChange `json:"changes"`
492
+ }
493
+
494
+ type patchApplyChange struct {
495
+ Type string `json:"type"`
496
+ }
497
+
498
+ type codexContentList struct {
499
+ Items []codexContentItem
500
+ }
501
+
502
+ func (c *codexContentList) UnmarshalJSON(data []byte) error {
503
+ if len(data) == 0 || string(data) == "null" {
504
+ return nil
505
+ }
506
+ if data[0] == '"' {
507
+ var s string
508
+ if err := json.Unmarshal(data, &s); err != nil {
509
+ return err
510
+ }
511
+ c.Items = []codexContentItem{{Type: "text", Text: s}}
512
+ return nil
513
+ }
514
+ var items []codexContentItem
515
+ if err := json.Unmarshal(data, &items); err != nil {
516
+ return err
517
+ }
518
+ c.Items = items
519
+ return nil
520
+ }
521
+
522
+ func (c codexContentList) Text() string {
523
+ var parts []string
524
+ for _, item := range c.Items {
525
+ if strings.TrimSpace(item.Text) != "" {
526
+ parts = append(parts, strings.TrimSpace(item.Text))
527
+ }
528
+ }
529
+ return strings.Join(parts, "\n")
530
+ }
531
+
532
+ func (c codexContentList) HasText() bool {
533
+ for _, item := range c.Items {
534
+ if strings.TrimSpace(item.Text) != "" {
535
+ return true
536
+ }
537
+ }
538
+ return false
539
+ }
540
+
541
+ type codexContentItem struct {
542
+ Type string `json:"type"`
543
+ Text string `json:"text"`
544
+ }
545
+
546
+ func applySessionMeta(meta *model.SessionMeta, payload sessionMetaPayload, setIdentity bool) {
547
+ if setIdentity {
548
+ if payload.ID != "" {
549
+ meta.ID = payload.ID
550
+ } else if payload.SessionID != "" {
551
+ meta.ID = payload.SessionID
552
+ }
553
+ meta.Agent = payload.agentMeta()
554
+ }
555
+ if payload.Cwd != "" && meta.Cwd == "" {
556
+ meta.Cwd = payload.Cwd
557
+ }
558
+ if payload.Git.Branch != "" && meta.GitBranch == "" {
559
+ meta.GitBranch = payload.Git.Branch
560
+ }
561
+ if payload.Timestamp != "" {
562
+ if meta.StartedAt == "" {
563
+ meta.StartedAt = payload.Timestamp
564
+ }
565
+ if meta.EndedAt == "" {
566
+ meta.EndedAt = payload.Timestamp
567
+ }
568
+ }
569
+ }
570
+
571
+ func applyTraceSessionMeta(trace *model.Trace, payload sessionMetaPayload, setIdentity bool) {
572
+ if setIdentity {
573
+ if payload.ID != "" {
574
+ trace.Session.ID = payload.ID
575
+ } else if payload.SessionID != "" {
576
+ trace.Session.ID = payload.SessionID
577
+ }
578
+ }
579
+ if payload.Cwd != "" && trace.Session.Cwd == "" {
580
+ trace.Session.Cwd = payload.Cwd
581
+ }
582
+ if payload.Git.CommitHash != "" && trace.Session.Commit == "" {
583
+ trace.Session.Commit = payload.Git.CommitHash
584
+ }
585
+ if payload.Timestamp != "" && trace.Session.StartedAt == "" {
586
+ trace.Session.StartedAt = payload.Timestamp
587
+ }
588
+ }
589
+
590
+ func applyLineTime(trace *model.Trace, ts string) {
591
+ if ts == "" {
592
+ return
593
+ }
594
+ if trace.Session.StartedAt == "" {
595
+ trace.Session.StartedAt = ts
596
+ }
597
+ trace.Session.EndedAt = ts
598
+ }
599
+
600
+ func decodeCall(payload responseItemPayload, timestamp string) (adapter.ToolCall, string, bool) {
601
+ callID, ok := canonicalCallID(payload.Type, payload.ID, payload.CallID, payload.Name)
602
+ if !ok {
603
+ return adapter.ToolCall{}, "", false
604
+ }
605
+ var raw json.RawMessage
606
+ switch payload.Type {
607
+ case "function_call":
608
+ raw = payload.Arguments
609
+ case "custom_tool_call":
610
+ raw = payload.Input
611
+ }
612
+ return adapter.ToolCall{
613
+ ID: callID,
614
+ Name: payload.Name,
615
+ Input: parseInput(raw),
616
+ Timestamp: timestamp,
617
+ }, payload.Type, true
618
+ }
619
+
620
+ func canonicalCallID(callType, id, callID, name string) (string, bool) {
621
+ switch callType {
622
+ case "function_call", "custom_tool_call":
623
+ default:
624
+ return "", false
625
+ }
626
+ if callID == "" {
627
+ callID = id
628
+ }
629
+ return callID, callID != "" && name != ""
630
+ }
631
+
632
+ func decodeOutput(payload responseItemPayload) (string, adapter.ToolResult, bool) {
633
+ switch payload.Type {
634
+ case "function_call_output", "custom_tool_call_output":
635
+ default:
636
+ return "", adapter.ToolResult{}, false
637
+ }
638
+ if payload.CallID == "" {
639
+ return "", adapter.ToolResult{}, false
640
+ }
641
+ output := adapter.ContentToString(payload.Output)
642
+ return payload.CallID, adapter.ToolResult{
643
+ Content: output,
644
+ }, true
645
+ }
646
+
647
+ func parseInput(raw json.RawMessage) map[string]any {
648
+ input := map[string]any{}
649
+ if len(raw) == 0 || string(raw) == "null" {
650
+ return input
651
+ }
652
+ var value any
653
+ if json.Unmarshal(raw, &value) != nil {
654
+ input["_raw"] = string(raw)
655
+ return input
656
+ }
657
+ switch value := value.(type) {
658
+ case map[string]any:
659
+ return value
660
+ case string:
661
+ return parseInputText(value)
662
+ default:
663
+ encoded, _ := json.Marshal(value)
664
+ input["_raw"] = string(encoded)
665
+ return input
666
+ }
667
+ }
668
+
669
+ func parseInputText(text string) map[string]any {
670
+ input := map[string]any{}
671
+ trimmed := strings.TrimSpace(text)
672
+ if trimmed == "" {
673
+ return input
674
+ }
675
+ if json.Unmarshal([]byte(trimmed), &input) == nil {
676
+ return input
677
+ }
678
+ var nested string
679
+ if json.Unmarshal([]byte(trimmed), &nested) == nil && nested != text {
680
+ return parseInputText(nested)
681
+ }
682
+ input["_raw"] = text
683
+ return input
684
+ }
685
+
686
+ func applyPatchChanges(input map[string]any, changes map[string]patchApplyChange) map[string]any {
687
+ if len(changes) == 0 {
688
+ return input
689
+ }
690
+ merged := make(map[string]any, len(input)+1)
691
+ for key, value := range input {
692
+ merged[key] = value
693
+ }
694
+ patch := ""
695
+ for _, key := range []string{"patch", "input", "_raw"} {
696
+ if value, ok := input[key].(string); ok {
697
+ patch = value
698
+ break
699
+ }
700
+ }
701
+ if patch != "" && !strings.HasSuffix(patch, "\n") {
702
+ patch += "\n"
703
+ }
704
+ paths := make([]string, 0, len(changes))
705
+ for path := range changes {
706
+ paths = append(paths, path)
707
+ }
708
+ sort.Strings(paths)
709
+ for _, path := range paths {
710
+ operation := "Update"
711
+ switch strings.ToLower(changes[path].Type) {
712
+ case "add":
713
+ operation = "Add"
714
+ case "delete":
715
+ operation = "Delete"
716
+ }
717
+ patch += fmt.Sprintf("*** %s File: %s\n", operation, path)
718
+ }
719
+ merged["patch"] = patch
720
+ return merged
721
+ }
722
+
723
+ var exitCodeRe = regexp.MustCompile(`(?im)^(?:Process exited with code|Exit code:)\s*([0-9]+)\s*$`)
724
+
725
+ func commandOutputStatus(output string) (failed bool, known bool) {
726
+ trimmed := strings.TrimSpace(output)
727
+ var envelope map[string]json.RawMessage
728
+ if json.Unmarshal([]byte(trimmed), &envelope) == nil {
729
+ if _, ok := exactString(envelope, "output"); ok {
730
+ if exitCode, ok := exactInt(envelope, "exit_code"); ok {
731
+ return exitCode != 0, true
732
+ }
733
+ if raw, ok := envelope["metadata"]; ok {
734
+ var metadata map[string]json.RawMessage
735
+ if json.Unmarshal(raw, &metadata) == nil {
736
+ if exitCode, ok := exactInt(metadata, "exit_code"); ok {
737
+ return exitCode != 0, true
738
+ }
739
+ }
740
+ }
741
+ }
742
+ if _, ok := exactString(envelope, "message"); ok {
743
+ if timedOut, ok := exactBool(envelope, "timed_out"); ok && timedOut {
744
+ return true, true
745
+ }
746
+ }
747
+ }
748
+ firstLine := trimmed
749
+ if newline := strings.IndexByte(firstLine, '\n'); newline >= 0 {
750
+ firstLine = firstLine[:newline]
751
+ }
752
+ status := strings.ToLower(strings.TrimSpace(firstLine))
753
+ if strings.HasPrefix(status, "script running with cell id ") {
754
+ return false, false
755
+ }
756
+ switch status {
757
+ case "script completed":
758
+ return false, true
759
+ case "script failed":
760
+ return true, true
761
+ case "script running":
762
+ return false, false
763
+ }
764
+ header := trimmed
765
+ for _, marker := range []string{"\nOutput:\n", "\nFinal output:\n"} {
766
+ if index := strings.Index(header, marker); index >= 0 {
767
+ header = header[:index]
768
+ }
769
+ }
770
+ for _, line := range strings.Split(header, "\n") {
771
+ if strings.EqualFold(strings.TrimSpace(line), "aborted by user") {
772
+ return true, true
773
+ }
774
+ }
775
+ match := exitCodeRe.FindStringSubmatch(header)
776
+ if len(match) == 2 {
777
+ return match[1] != "0", true
778
+ }
779
+ return false, false
780
+ }
781
+
782
+ func toolOutputStatus(tool, output string) (failed bool, known bool) {
783
+ switch tool {
784
+ case "exec", "exec_command", "write_stdin", "wait":
785
+ return commandOutputStatus(output)
786
+ case "apply_patch":
787
+ if strings.HasPrefix(strings.ToLower(strings.TrimSpace(output)), "apply_patch verification failed") {
788
+ return true, true
789
+ }
790
+ }
791
+ return false, false
792
+ }
793
+
794
+ func exactBoolPointer(data json.RawMessage, key string) *bool {
795
+ var fields map[string]json.RawMessage
796
+ if json.Unmarshal(data, &fields) != nil {
797
+ return nil
798
+ }
799
+ value, ok := exactBool(fields, key)
800
+ if !ok {
801
+ return nil
802
+ }
803
+ return &value
804
+ }
805
+
806
+ func exactBool(fields map[string]json.RawMessage, key string) (bool, bool) {
807
+ raw, ok := fields[key]
808
+ if !ok || strings.TrimSpace(string(raw)) == "null" {
809
+ return false, false
810
+ }
811
+ var value bool
812
+ if json.Unmarshal(raw, &value) != nil {
813
+ return false, false
814
+ }
815
+ return value, true
816
+ }
817
+
818
+ func exactInt(fields map[string]json.RawMessage, key string) (int, bool) {
819
+ raw, ok := fields[key]
820
+ if !ok || strings.TrimSpace(string(raw)) == "null" {
821
+ return 0, false
822
+ }
823
+ var value int
824
+ if json.Unmarshal(raw, &value) != nil {
825
+ return 0, false
826
+ }
827
+ return value, true
828
+ }
829
+
830
+ func exactString(fields map[string]json.RawMessage, key string) (string, bool) {
831
+ raw, ok := fields[key]
832
+ if !ok || strings.TrimSpace(string(raw)) == "null" {
833
+ return "", false
834
+ }
835
+ var value string
836
+ if json.Unmarshal(raw, &value) != nil {
837
+ return "", false
838
+ }
839
+ return value, true
840
+ }
841
+
842
+ func (a Adapter) titleFor(id string) string {
843
+ if id == "" {
844
+ return ""
845
+ }
846
+ index := a.indexPath()
847
+ if index == "" {
848
+ return ""
849
+ }
850
+ return loadTitleIndex(index)[id]
851
+ }
852
+
853
+ // titleIndexCache holds the parsed session index; summarizing a whole
854
+ // sessions directory looks up a title per file, so re-reading the index
855
+ // each time would make the scan quadratic
856
+ var titleIndexCache struct {
857
+ mu sync.Mutex
858
+ path string
859
+ size int64
860
+ modTime time.Time
861
+ titles map[string]string
862
+ }
863
+
864
+ func loadTitleIndex(path string) map[string]string {
865
+ info, err := os.Stat(path)
866
+ if err != nil {
867
+ return nil
868
+ }
869
+ titleIndexCache.mu.Lock()
870
+ defer titleIndexCache.mu.Unlock()
871
+ if titleIndexCache.path == path && titleIndexCache.size == info.Size() && titleIndexCache.modTime.Equal(info.ModTime()) {
872
+ return titleIndexCache.titles
873
+ }
874
+ f, err := os.Open(path)
875
+ if err != nil {
876
+ return nil
877
+ }
878
+ defer f.Close()
879
+
880
+ titles := map[string]string{}
881
+ _ = adapter.ReadJSONLines(f, func(data []byte) {
882
+ var row struct {
883
+ ID string `json:"id"`
884
+ ThreadName string `json:"thread_name"`
885
+ }
886
+ if json.Unmarshal(data, &row) == nil && row.ID != "" && row.ThreadName != "" {
887
+ titles[row.ID] = row.ThreadName
888
+ }
889
+ })
890
+ titleIndexCache.path = path
891
+ titleIndexCache.size = info.Size()
892
+ titleIndexCache.modTime = info.ModTime()
893
+ titleIndexCache.titles = titles
894
+ return titles
895
+ }
896
+
897
+ func (a Adapter) indexPath() string {
898
+ if a.IndexPath != "" {
899
+ return a.IndexPath
900
+ }
901
+ if a.Dir != "" {
902
+ dir := filepath.Clean(a.SessionDir())
903
+ candidate := filepath.Join(filepath.Dir(dir), "session_index.jsonl")
904
+ if _, err := os.Stat(candidate); err == nil {
905
+ return candidate
906
+ }
907
+ }
908
+ if candidate := DefaultIndexPath(); candidate != "" {
909
+ if _, err := os.Stat(candidate); err == nil {
910
+ return candidate
911
+ }
912
+ }
913
+ dir := filepath.Clean(a.SessionDir())
914
+ candidate := filepath.Join(filepath.Dir(dir), "session_index.jsonl")
915
+ if _, err := os.Stat(candidate); err == nil {
916
+ return candidate
917
+ }
918
+ return ""
919
+ }