ScanJSONL reads a Claude session jsonl byte-stream and extracts a FileRollup. ownSlug is the slug of the task that owns this transcript, used to distinguish own-bootstrap reads (skipped) from sibling reads (cross_task). Malformed lines are skipped silently.
(r io.Reader, ownSlug string)
| 83 | // used to distinguish own-bootstrap reads (skipped) from sibling reads |
| 84 | // (cross_task). Malformed lines are skipped silently. |
| 85 | func ScanJSONL(r io.Reader, ownSlug string) (FileRollup, error) { |
| 86 | var roll FileRollup |
| 87 | ownPrefix := "/.flow/tasks/" + ownSlug + "/" |
| 88 | |
| 89 | sc := bufio.NewScanner(r) |
| 90 | sc.Buffer(make([]byte, 0, 64*1024), 10*1024*1024) |
| 91 | for sc.Scan() { |
| 92 | line := sc.Bytes() |
| 93 | if len(line) == 0 { |
| 94 | continue |
| 95 | } |
| 96 | var rec rawRecord |
| 97 | if err := json.Unmarshal(line, &rec); err != nil { |
| 98 | continue |
| 99 | } |
| 100 | ts := parseTS(rec.Timestamp) |
| 101 | if !ts.IsZero() { |
| 102 | if roll.First.IsZero() || ts.Before(roll.First) { |
| 103 | roll.First = ts |
| 104 | } |
| 105 | if ts.After(roll.Last) { |
| 106 | roll.Last = ts |
| 107 | } |
| 108 | } |
| 109 | if len(rec.Message) == 0 { |
| 110 | continue |
| 111 | } |
| 112 | var msg rawMessage |
| 113 | if err := json.Unmarshal(rec.Message, &msg); err != nil { |
| 114 | continue |
| 115 | } |
| 116 | if msg.Usage != nil { |
| 117 | roll.Usage.Input += msg.Usage.Input |
| 118 | roll.Usage.Output += msg.Usage.Output |
| 119 | roll.Usage.CacheCreation += msg.Usage.CacheCreation |
| 120 | roll.Usage.CacheRead += msg.Usage.CacheRead |
| 121 | } |
| 122 | if len(msg.Content) == 0 { |
| 123 | continue |
| 124 | } |
| 125 | var blocks []rawBlock |
| 126 | if err := json.Unmarshal(msg.Content, &blocks); err != nil { |
| 127 | continue |
| 128 | } |
| 129 | for _, b := range blocks { |
| 130 | if b.Type != "tool_use" || len(b.Input) == 0 { |
| 131 | continue |
| 132 | } |
| 133 | var in rawInput |
| 134 | if err := json.Unmarshal(b.Input, &in); err != nil { |
| 135 | continue |
| 136 | } |
| 137 | if kind, ok := classify(b.Name, in, ownPrefix); ok { |
| 138 | roll.Lookups = append(roll.Lookups, Lookup{Kind: kind, TS: ts}) |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | if err := sc.Err(); err != nil { |