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.
- package/README.md +11 -1
- package/README.zh-CN.md +11 -1
- package/core/types.ts +10 -0
- package/dist/ui/assets/index-DghP-SmR.js +32 -0
- package/dist/ui/assets/index-KMSTKoNg.css +1 -0
- package/dist/ui/index.html +3 -3
- package/package.json +1 -1
- package/runtime-node/cli-ingest.ts +2 -2
- package/runtime-node/dev-server.ts +1 -1
- package/runtime-node/ingest-worker.ts +3 -3
- package/runtime-node/ingest.ts +33 -5
- package/runtime-node/prod-server.ts +1 -1
- package/runtime-node/scanner.ts +15 -7
- package/runtime-node/server.ts +2 -2
- package/storage/database.ts +182 -3
- package/dist/ui/assets/index-CCbiSQCw.js +0 -32
- package/dist/ui/assets/index-CoO16M0D.css +0 -1
package/storage/database.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|