cc-flight 0.5.0 → 0.6.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.
@@ -15,6 +15,8 @@ import {
15
15
  RawEventRef,
16
16
  RunReplay,
17
17
  SessionRecord,
18
+ TaskSubThread,
19
+ TaskJourney,
18
20
  TaskJourneyDetail,
19
21
  TokenUsage,
20
22
  TimelineQuery,
@@ -33,7 +35,7 @@ type EventRow = Omit<TimelineEvent, "files" | "tokenUsage" | "skills"> & {
33
35
  skillsJson: string | null;
34
36
  };
35
37
 
36
- export class SuperViewDatabase {
38
+ export class CCFlightDatabase {
37
39
  private db: Database.Database;
38
40
 
39
41
  constructor(databasePath = resolveDatabasePath()) {
@@ -654,8 +656,14 @@ export class SuperViewDatabase {
654
656
  if (!project) return null;
655
657
  const events = this.listEvents(projectId, query);
656
658
  const timeline = buildProjectTimeline(project, events);
659
+ const subagentEventIds = this.subagentEventIds(projectId);
660
+ const taskJourneys = this.withSubThreadCounts(
661
+ timeline.taskJourneys.filter((journey) => !subagentEventIds.has(journey.promptEventId)),
662
+ projectId,
663
+ );
657
664
  return {
658
665
  ...timeline,
666
+ taskJourneys,
659
667
  episodes: this.listEpisodes(projectId),
660
668
  tokenUsage: this.getProjectTokenUsage(projectId),
661
669
  totalEvents: this.countEvents(projectId, query),
@@ -727,19 +735,128 @@ export class SuperViewDatabase {
727
735
  for (const project of projects) {
728
736
  const events = this.listEvents(project.id);
729
737
  const timeline = buildProjectTimeline(project, events);
730
- const journey = timeline.taskJourneys.find((candidate) => candidate.id === journeyId);
738
+ const subagentEventIds = this.subagentEventIds(project.id);
739
+ const taskJourneys = this.withSubThreadCounts(
740
+ timeline.taskJourneys.filter((candidate) => !subagentEventIds.has(candidate.promptEventId)),
741
+ project.id,
742
+ );
743
+ const journey = taskJourneys
744
+ .find((candidate) => candidate.id === journeyId);
731
745
  if (!journey) continue;
732
746
  const eventIds = new Set(journey.eventIds);
733
747
  const journeyEvents = events.filter((event) => eventIds.has(event.id));
734
748
  return {
735
749
  journey,
736
750
  events: journeyEvents,
737
- causalEdges: timeline.causalEdges.filter((edge) => eventIds.has(edge.fromEventId) || eventIds.has(edge.toEventId))
751
+ causalEdges: timeline.causalEdges.filter((edge) => eventIds.has(edge.fromEventId) || eventIds.has(edge.toEventId)),
752
+ subThreads: this.listSubThreadsForJourney(journey)
738
753
  };
739
754
  }
740
755
  return null;
741
756
  }
742
757
 
758
+ private listSubThreadsForJourney(journey: TaskJourneyDetail["journey"]): TaskSubThread[] {
759
+ const parentSession = this.getSession(journey.sessionId);
760
+ const parentKey = parentSession?.externalSessionId || stripProviderPrefix(journey.sessionId);
761
+ if (!parentKey) return [];
762
+ const escapedLike = escapeSqlLike(parentKey);
763
+ const endClause = journey.exitType === "next_prompt" ? "AND e.timestamp <= ?" : "";
764
+ const params = [
765
+ `%/${escapedLike}/subagents/%`,
766
+ `%\\${escapedLike}\\subagents\\%`,
767
+ journey.startedAt,
768
+ ...(journey.exitType === "next_prompt" ? [journey.endedAt] : [])
769
+ ];
770
+ const sourceRows = this.db
771
+ .prepare(
772
+ `SELECT r.source_path as sourcePath
773
+ FROM raw_event_refs r
774
+ JOIN events e ON e.raw_event_ref_id = r.id
775
+ WHERE (r.source_path LIKE ? ESCAPE '\\' OR r.source_path LIKE ? ESCAPE '\\')
776
+ AND e.timestamp >= ?
777
+ ${endClause}
778
+ GROUP BY r.source_path
779
+ ORDER BY MIN(e.timestamp) ASC`
780
+ )
781
+ .all(...params) as Array<{ sourcePath: string }>;
782
+
783
+ return sourceRows.flatMap((row, index) => {
784
+ const events = this.listEventsForSourcePath(row.sourcePath);
785
+ if (events.length === 0) return [];
786
+ const subProject = this.getProject(events[0].projectId);
787
+ if (!subProject) return [];
788
+ const subTimeline = buildProjectTimeline(subProject, events);
789
+ const subJourney = subTimeline.taskJourneys[0] ?? taskJourneyFromEvents(subProject.id, row.sourcePath, events);
790
+ if (!subJourney) return [];
791
+ return [
792
+ {
793
+ id: `${journey.id}:subthread:${index}`,
794
+ sourcePath: row.sourcePath,
795
+ session: this.getSession(events[0].sessionId),
796
+ journey: subJourney,
797
+ events
798
+ }
799
+ ];
800
+ });
801
+ }
802
+
803
+ private withSubThreadCounts(journeys: TaskJourney[], projectId: string): TaskJourney[] {
804
+ if (journeys.length === 0) return journeys;
805
+ const sessionsById = new Map(this.listSessions(projectId).map((session) => [session.id, session]));
806
+ const subagentSources = this.listSubagentSourceSummaries();
807
+ if (subagentSources.length === 0) return journeys.map((journey) => ({ ...journey, subThreadCount: 0 }));
808
+ return journeys.map((journey) => {
809
+ const parentSession = sessionsById.get(journey.sessionId);
810
+ const parentKey = parentSession?.externalSessionId || stripProviderPrefix(journey.sessionId);
811
+ const subThreadCount = parentKey
812
+ ? subagentSources.filter((source) => isSourceForParentSession(source.sourcePath, parentKey) && isSourceInJourneyWindow(source.startedAt, journey)).length
813
+ : 0;
814
+ return { ...journey, subThreadCount };
815
+ });
816
+ }
817
+
818
+ private listSubagentSourceSummaries(): Array<{ sourcePath: string; startedAt: string }> {
819
+ return this.db
820
+ .prepare(
821
+ `SELECT r.source_path as sourcePath, MIN(e.timestamp) as startedAt
822
+ FROM raw_event_refs r
823
+ JOIN events e ON e.raw_event_ref_id = r.id
824
+ WHERE r.source_path LIKE '%/subagents/%' OR r.source_path LIKE '%\\subagents\\%'
825
+ GROUP BY r.source_path
826
+ ORDER BY MIN(e.timestamp) ASC`
827
+ )
828
+ .all() as Array<{ sourcePath: string; startedAt: string }>;
829
+ }
830
+
831
+ private listEventsForSourcePath(sourcePath: string): TimelineEvent[] {
832
+ const rows = this.db
833
+ .prepare(
834
+ `SELECT e.id, e.project_id as projectId, e.session_id as sessionId, e.turn_id as turnId, e.timestamp, e.kind, e.lane, e.title, e.detail,
835
+ e.tool_name as toolName, e.call_id as callId, e.status, e.files_json as filesJson, e.raw_event_ref_id as rawEventRefId,
836
+ e.duration_ms as durationMs, e.output_event_id as outputEventId, e.commit_hash as commitHash, e.token_usage_json as tokenUsageJson,
837
+ e.skills_json as skillsJson
838
+ FROM events e
839
+ JOIN raw_event_refs r ON r.id = e.raw_event_ref_id
840
+ WHERE r.source_path = ?
841
+ ORDER BY e.timestamp ASC`
842
+ )
843
+ .all(sourcePath) as EventRow[];
844
+ return rows.map(rowToTimelineEvent);
845
+ }
846
+
847
+ private subagentEventIds(projectId: string): Set<string> {
848
+ const rows = this.db
849
+ .prepare(
850
+ `SELECT e.id
851
+ FROM events e
852
+ JOIN raw_event_refs r ON r.id = e.raw_event_ref_id
853
+ WHERE e.project_id = ?
854
+ AND (r.source_path LIKE '%/subagents/%' OR r.source_path LIKE '%\\subagents\\%')`
855
+ )
856
+ .all(projectId) as Array<{ id: string }>;
857
+ return new Set(rows.map((row) => row.id));
858
+ }
859
+
743
860
  getEventEvidenceByEventIds(eventIds: string[]): Record<string, EventEvidence> {
744
861
  if (eventIds.length === 0) return {};
745
862
  const events = eventIds.map((eventId) => this.getEvent(eventId)).filter((event): event is TimelineEvent => Boolean(event));
@@ -1005,6 +1122,68 @@ function parseSkills(value: string | null): TimelineEvent["skills"] {
1005
1122
  }
1006
1123
  }
1007
1124
 
1125
+ function taskJourneyFromEvents(projectId: string, sourcePath: string, events: TimelineEvent[]): TaskJourney | null {
1126
+ const first = events[0];
1127
+ const last = events.at(-1);
1128
+ if (!first || !last) return null;
1129
+ return {
1130
+ id: `subthread:${projectId}:${sourcePath}`,
1131
+ projectId,
1132
+ sessionId: first.sessionId,
1133
+ promptEventId: first.id,
1134
+ startedAt: first.timestamp,
1135
+ endedAt: last.timestamp,
1136
+ durationMs: durationBetween(first.timestamp, last.timestamp),
1137
+ title: first.detail ?? first.title,
1138
+ summary: `Subagent thread with ${events.length} event(s).`,
1139
+ status: events.some((event) => event.status === "failed") ? "failed" : events.some((event) => event.status === "success") ? "success" : "unknown",
1140
+ exitType: "session_end",
1141
+ eventIds: events.map((event) => event.id),
1142
+ tokenUsage: aggregateEventTokenUsage(events),
1143
+ skills: [],
1144
+ stageCounts: {},
1145
+ stages: []
1146
+ };
1147
+ }
1148
+
1149
+ function aggregateEventTokenUsage(events: TimelineEvent[]): TokenUsage {
1150
+ return events.reduce<TokenUsage>(
1151
+ (total, event) => ({
1152
+ input: total.input + (event.tokenUsage?.input ?? 0),
1153
+ output: total.output + (event.tokenUsage?.output ?? 0),
1154
+ reasoning: total.reasoning + (event.tokenUsage?.reasoning ?? 0),
1155
+ cachedInput: total.cachedInput + (event.tokenUsage?.cachedInput ?? 0),
1156
+ total: total.total + (event.tokenUsage?.total ?? 0)
1157
+ }),
1158
+ emptyTokenUsage()
1159
+ );
1160
+ }
1161
+
1162
+ function durationBetween(start: string, end: string): number {
1163
+ const startMs = Date.parse(start);
1164
+ const endMs = Date.parse(end);
1165
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return 0;
1166
+ return Math.max(0, endMs - startMs);
1167
+ }
1168
+
1169
+ function stripProviderPrefix(sessionId: string): string {
1170
+ const index = sessionId.indexOf(":");
1171
+ return index >= 0 ? sessionId.slice(index + 1) : sessionId;
1172
+ }
1173
+
1174
+ function isSourceForParentSession(sourcePath: string, parentKey: string): boolean {
1175
+ return sourcePath.includes(`/${parentKey}/subagents/`) || sourcePath.includes(`\\${parentKey}\\subagents\\`);
1176
+ }
1177
+
1178
+ function isSourceInJourneyWindow(startedAt: string, journey: TaskJourney): boolean {
1179
+ if (startedAt < journey.startedAt) return false;
1180
+ return journey.exitType !== "next_prompt" || startedAt <= journey.endedAt;
1181
+ }
1182
+
1183
+ function escapeSqlLike(value: string): string {
1184
+ return value.replace(/[\\%_]/g, (match) => `\\${match}`);
1185
+ }
1186
+
1008
1187
  const ALL_AGENT_PROVIDERS: AgentProvider[] = ["codex", "claude-code", "opencode"];
1009
1188
 
1010
1189
  function sourcePathFromIngestedId(sourceId: string): string {